claude-dev-env 8.88.7 → 8.90.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/pr-visual/README.md +29 -0
- package/.agents/skills/pr-visual/SKILL.md +62 -0
- package/.agents/skills/pr-visual/assets/icons.symbols +27 -0
- package/.agents/skills/pr-visual/assets/viewer.html +147 -0
- package/.agents/skills/pr-visual/fixtures/bar.scene.json +193 -0
- package/.agents/skills/pr-visual/fixtures/flow.scene.json +201 -0
- package/.agents/skills/pr-visual/fixtures/head/metrics.json +16 -0
- package/.agents/skills/pr-visual/fixtures/integrated.scene.json +239 -0
- package/.agents/skills/pr-visual/fixtures/line.scene.json +193 -0
- package/.agents/skills/pr-visual/fixtures/scatter.scene.json +193 -0
- package/.agents/skills/pr-visual/fixtures/snapshot.json +21 -0
- package/.agents/skills/pr-visual/references/evidence.md +9 -0
- package/.agents/skills/pr-visual/references/formats.md +30 -0
- package/.agents/skills/pr-visual/references/scene-contract.md +48 -0
- package/.agents/skills/pr-visual/references/visual-contract.md +23 -0
- package/.agents/skills/pr-visual/scripts/collect.py +111 -0
- package/.agents/skills/pr-visual/scripts/metrics.py +99 -0
- package/.agents/skills/pr-visual/scripts/render.py +133 -0
- package/.agents/skills/pr-visual/scripts/test_collect.py +58 -0
- package/.agents/skills/pr-visual/scripts/test_metrics.py +47 -0
- package/.agents/skills/pr-visual/scripts/test_render.py +77 -0
- package/.agents/skills/pr-visual/scripts/verify.py +218 -0
- package/.agents/skills/pr-visual/scripts/visual_config/config/constants.py +104 -0
- package/bin/codex-skill-load-block.mjs +0 -25
- package/bin/codex-skill-load-block.test.mjs +5 -17
- package/bin/codex-trimmed-sol.test.mjs +0 -1
- package/bin/cursor-native-hooks.test.mjs +3 -3
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/install-constants.mjs +3 -0
- package/bin/install-deepstack-plugin.mjs +42 -25
- package/bin/install-deepstack-plugin.test.mjs +37 -11
- package/bin/install-pstack-plugin.mjs +58 -11
- package/bin/install-pstack-plugin.test.mjs +54 -21
- package/bin/install.agents-home.test.mjs +0 -1
- package/bin/install.codex-rules.test.mjs +0 -2
- package/bin/install.cursor-rules.test.mjs +96 -21
- package/bin/install.mjs +48 -44
- package/bin/install.pr-visual.test.mjs +40 -0
- package/bin/install.prompt-context.test.mjs +3 -4
- package/bin/install.prune.test.mjs +0 -1
- package/bin/install.question-presentation.test.mjs +0 -4
- package/bin/install.settings-defaults.test.mjs +0 -1
- package/bin/install.shared-settings.test.mjs +0 -1
- package/bin/install.test.mjs +14 -44
- package/bin/install.transaction.test.mjs +0 -8
- package/bin/install.uninstall-transaction.test.mjs +0 -2
- package/bin/install.update-keeps-hooks.test.mjs +0 -1
- package/docs/BDD_SCENARIO_QUALITY.md +1 -1
- package/docs/BDD_TEST_LAYOUT.md +1 -1
- package/docs/CODE_RULES.md +1 -1
- package/docs/rule-guides/deepstack-wording.md +3 -5
- package/docs/rule-guides/session-title.md +5 -4
- package/hooks/advisory/owned_pr_review_loop.py +365 -0
- package/hooks/advisory/test_owned_pr_review_loop.py +369 -0
- package/hooks/blocking/session_title_turn_gate.py +287 -0
- package/hooks/blocking/test_session_title_format_gate.py +5 -0
- package/hooks/blocking/test_session_title_turn_gate.py +264 -0
- package/hooks/features/bash-dispatchers.md +3 -0
- package/hooks/features/conduct-gates.md +3 -0
- package/hooks/features/session-context.md +0 -4
- package/hooks/hooks.json +15 -5
- package/hooks/hooks_constants/mod_handoff_constants.py +0 -2
- package/hooks/hooks_constants/owned_pr_review_loop_constants.py +120 -0
- package/hooks/hooks_constants/session_title_constants.py +41 -3
- package/hooks/lifecycle/nested_project_hooks.py +2 -2
- package/hooks/lifecycle/test_nested_project_hooks.py +2 -2
- package/hooks/session/compaction_user_messages.py +2 -2
- package/package.json +2 -2
- package/rules/deepstack-wording.md +3 -3
- package/scripts/ci/install-playtest.mjs +58 -314
- package/scripts/ci/install-playtest.test.mjs +24 -6
- package/scripts/policy_lint/adapter_configuration.py +1 -0
- package/scripts/policy_lint/config/constants.py +1 -0
- package/scripts/test_deepstack_review_criterion.py +2 -3
- package/scripts/test_deepstack_wording_rule.py +6 -4
- package/scripts/tests/test_adapter_configuration.py +8 -0
- package/.agents/skills/deepen/SKILL.md +0 -26
- package/commands/deepen.md +0 -14
- package/docs/decisions/deepstack-gate.md +0 -32
- package/hooks/hooks_constants/deepen_first_prompt_constants.py +0 -187
- package/hooks/session/deepen_default_rewrite.py +0 -128
- package/hooks/session/deepen_first_prompt.py +0 -383
- package/hooks/session/deepen_role_gate.py +0 -171
- package/hooks/session/deepen_turn_marker.py +0 -75
- package/hooks/session/fixtures/deepstack_replay.json +0 -15
- package/hooks/session/grade_deepstack_replay.py +0 -78
- package/hooks/session/test_deepen_default_rewrite.py +0 -90
- package/hooks/session/test_deepen_first_prompt.py +0 -452
- package/hooks/session/test_deepen_role_gate.py +0 -83
- package/hooks/session/test_deepen_turn_marker.py +0 -59
- package/hooks/session/test_grade_deepstack_replay.py +0 -47
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
import session_title_turn_gate
|
|
8
|
+
|
|
9
|
+
REMOTE_TITLE_TOOL = "mcp__claude-code-remote__set_session_title"
|
|
10
|
+
DESKTOP_TITLE_TOOL = "mcp__ccd_session_mgmt__set_session_title"
|
|
11
|
+
TOOL_LISTING = {
|
|
12
|
+
"type": "attachment",
|
|
13
|
+
"attachment": {
|
|
14
|
+
"type": "prompt_snapshot",
|
|
15
|
+
"tools": [{"name": "Bash"}, {"name": REMOTE_TITLE_TOOL}],
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
DEFERRED_LISTING = {
|
|
19
|
+
"type": "attachment",
|
|
20
|
+
"attachment": {"type": "deferred_tools_delta", "addedNames": [DESKTOP_TITLE_TOOL]},
|
|
21
|
+
}
|
|
22
|
+
PROMPT = {"type": "user", "message": {"role": "user", "content": "Fix the gate"}}
|
|
23
|
+
WORK_CALL = {
|
|
24
|
+
"type": "assistant",
|
|
25
|
+
"message": {"content": [{"type": "tool_use", "id": "w1", "name": "Bash", "input": {}}]},
|
|
26
|
+
}
|
|
27
|
+
WORK_RESULT = {
|
|
28
|
+
"type": "user",
|
|
29
|
+
"message": {"content": [{"type": "tool_result", "tool_use_id": "w1", "content": "ok"}]},
|
|
30
|
+
}
|
|
31
|
+
REPLY = {"type": "assistant", "message": {"content": [{"type": "text", "text": "Done."}]}}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def title_call(tool_use_id: str, tool_name: str = REMOTE_TITLE_TOOL) -> dict[str, object]:
|
|
35
|
+
return {
|
|
36
|
+
"type": "assistant",
|
|
37
|
+
"message": {
|
|
38
|
+
"content": [
|
|
39
|
+
{
|
|
40
|
+
"type": "tool_use",
|
|
41
|
+
"id": tool_use_id,
|
|
42
|
+
"name": tool_name,
|
|
43
|
+
"input": {"title": "✅ Gate"},
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def title_result(tool_use_id: str, is_error: bool = False) -> dict[str, object]:
|
|
51
|
+
return {
|
|
52
|
+
"type": "user",
|
|
53
|
+
"message": {
|
|
54
|
+
"content": [
|
|
55
|
+
{
|
|
56
|
+
"type": "tool_result",
|
|
57
|
+
"tool_use_id": tool_use_id,
|
|
58
|
+
"content": "ok",
|
|
59
|
+
"is_error": is_error,
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def run_gate(
|
|
67
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
68
|
+
capsys: pytest.CaptureFixture[str],
|
|
69
|
+
tmp_path: Path,
|
|
70
|
+
all_entries: list[dict[str, object]],
|
|
71
|
+
is_stop_hook_active: bool = False,
|
|
72
|
+
) -> tuple[int, str]:
|
|
73
|
+
transcript_path = tmp_path / "transcript.jsonl"
|
|
74
|
+
transcript_path.write_text(
|
|
75
|
+
"\n".join(json.dumps(each) for each in all_entries) + "\n", encoding="utf-8"
|
|
76
|
+
)
|
|
77
|
+
hook_input = {
|
|
78
|
+
"hook_event_name": "Stop",
|
|
79
|
+
"stop_hook_active": is_stop_hook_active,
|
|
80
|
+
"transcript_path": str(transcript_path),
|
|
81
|
+
}
|
|
82
|
+
monkeypatch.setattr(
|
|
83
|
+
"sys.stdin", io.TextIOWrapper(io.BytesIO(json.dumps(hook_input).encode("utf-8")))
|
|
84
|
+
)
|
|
85
|
+
exit_code = session_title_turn_gate.main()
|
|
86
|
+
return exit_code, capsys.readouterr().out
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@pytest.fixture(autouse=True)
|
|
90
|
+
def isolated_home(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
91
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
92
|
+
monkeypatch.setenv("USERPROFILE", str(tmp_path))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_should_hold_a_turn_that_set_no_title(
|
|
96
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
|
|
97
|
+
) -> None:
|
|
98
|
+
exit_code, stdout_text = run_gate(
|
|
99
|
+
monkeypatch, capsys, tmp_path, [TOOL_LISTING, PROMPT, WORK_CALL, WORK_RESULT, REPLY]
|
|
100
|
+
)
|
|
101
|
+
block_output = json.loads(stdout_text)
|
|
102
|
+
assert exit_code == 0
|
|
103
|
+
assert block_output["decision"] == "block"
|
|
104
|
+
assert REMOTE_TITLE_TOOL in block_output["reason"]
|
|
105
|
+
assert all(
|
|
106
|
+
each_emoji in block_output["reason"]
|
|
107
|
+
for each_emoji in ("\U0001f6a9", "\U0001f6a7", "✅", "⏳")
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_should_pass_a_turn_that_set_a_title(
|
|
112
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
|
|
113
|
+
) -> None:
|
|
114
|
+
all_entries = [
|
|
115
|
+
TOOL_LISTING,
|
|
116
|
+
PROMPT,
|
|
117
|
+
WORK_CALL,
|
|
118
|
+
WORK_RESULT,
|
|
119
|
+
title_call("t1"),
|
|
120
|
+
title_result("t1"),
|
|
121
|
+
REPLY,
|
|
122
|
+
]
|
|
123
|
+
assert run_gate(monkeypatch, capsys, tmp_path, all_entries) == (0, "")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_should_hold_a_turn_whose_title_call_was_denied(
|
|
127
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
|
|
128
|
+
) -> None:
|
|
129
|
+
all_entries = [TOOL_LISTING, PROMPT, title_call("t1"), title_result("t1", is_error=True), REPLY]
|
|
130
|
+
assert (
|
|
131
|
+
json.loads(run_gate(monkeypatch, capsys, tmp_path, all_entries)[1])["decision"] == "block"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_should_pass_a_held_turn_whose_title_call_failed(
|
|
136
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
|
|
137
|
+
) -> None:
|
|
138
|
+
all_entries = [TOOL_LISTING, PROMPT, title_call("t1"), title_result("t1", is_error=True), REPLY]
|
|
139
|
+
assert run_gate(monkeypatch, capsys, tmp_path, all_entries, is_stop_hook_active=True) == (
|
|
140
|
+
0,
|
|
141
|
+
"",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_should_hold_a_held_turn_with_no_title_call(
|
|
146
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
|
|
147
|
+
) -> None:
|
|
148
|
+
all_entries = [TOOL_LISTING, PROMPT, WORK_CALL, WORK_RESULT, REPLY]
|
|
149
|
+
stdout_text = run_gate(monkeypatch, capsys, tmp_path, all_entries, is_stop_hook_active=True)[1]
|
|
150
|
+
assert json.loads(stdout_text)["decision"] == "block"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_should_hold_when_the_title_was_set_only_in_an_earlier_turn(
|
|
154
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
|
|
155
|
+
) -> None:
|
|
156
|
+
all_entries = [
|
|
157
|
+
PROMPT,
|
|
158
|
+
title_call("t1"),
|
|
159
|
+
title_result("t1"),
|
|
160
|
+
REPLY,
|
|
161
|
+
PROMPT,
|
|
162
|
+
WORK_CALL,
|
|
163
|
+
WORK_RESULT,
|
|
164
|
+
REPLY,
|
|
165
|
+
]
|
|
166
|
+
block_output = json.loads(run_gate(monkeypatch, capsys, tmp_path, all_entries)[1])
|
|
167
|
+
assert block_output["decision"] == "block"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_should_start_a_turn_at_an_image_only_prompt(
|
|
171
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
|
|
172
|
+
) -> None:
|
|
173
|
+
image_prompt = {
|
|
174
|
+
"type": "user",
|
|
175
|
+
"message": {"content": [{"type": "image", "source": {"type": "base64", "data": ""}}]},
|
|
176
|
+
}
|
|
177
|
+
all_entries = [
|
|
178
|
+
TOOL_LISTING,
|
|
179
|
+
PROMPT,
|
|
180
|
+
title_call("t1"),
|
|
181
|
+
title_result("t1"),
|
|
182
|
+
REPLY,
|
|
183
|
+
image_prompt,
|
|
184
|
+
REPLY,
|
|
185
|
+
]
|
|
186
|
+
stdout_text = run_gate(monkeypatch, capsys, tmp_path, all_entries)[1]
|
|
187
|
+
assert json.loads(stdout_text)["decision"] == "block"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_should_hold_a_turn_whose_only_title_call_ran_on_a_sidechain(
|
|
191
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
|
|
192
|
+
) -> None:
|
|
193
|
+
sidechain_call = {**title_call("s1"), "isSidechain": True}
|
|
194
|
+
sidechain_result = {**title_result("s1"), "isSidechain": True}
|
|
195
|
+
all_entries = [TOOL_LISTING, PROMPT, sidechain_call, sidechain_result, REPLY]
|
|
196
|
+
stdout_text = run_gate(monkeypatch, capsys, tmp_path, all_entries)[1]
|
|
197
|
+
assert json.loads(stdout_text)["decision"] == "block"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_should_keep_the_turn_across_a_stop_block_and_a_meta_entry(
|
|
201
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
|
|
202
|
+
) -> None:
|
|
203
|
+
stop_block = {
|
|
204
|
+
"type": "attachment",
|
|
205
|
+
"attachment": {"type": "hook_blocking_error", "hookEvent": "Stop"},
|
|
206
|
+
}
|
|
207
|
+
meta_entry = {"type": "user", "isMeta": True, "message": {"content": "Stop hook feedback"}}
|
|
208
|
+
all_entries = [
|
|
209
|
+
TOOL_LISTING,
|
|
210
|
+
PROMPT,
|
|
211
|
+
title_call("t1"),
|
|
212
|
+
title_result("t1"),
|
|
213
|
+
REPLY,
|
|
214
|
+
stop_block,
|
|
215
|
+
meta_entry,
|
|
216
|
+
REPLY,
|
|
217
|
+
]
|
|
218
|
+
assert run_gate(monkeypatch, capsys, tmp_path, all_entries) == (0, "")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_should_name_a_deferred_title_tool_in_the_reason(
|
|
222
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
|
|
223
|
+
) -> None:
|
|
224
|
+
block_output = json.loads(
|
|
225
|
+
run_gate(monkeypatch, capsys, tmp_path, [DEFERRED_LISTING, PROMPT, REPLY])[1]
|
|
226
|
+
)
|
|
227
|
+
assert DESKTOP_TITLE_TOOL in block_output["reason"]
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@pytest.mark.parametrize(
|
|
231
|
+
"all_entries",
|
|
232
|
+
[
|
|
233
|
+
[PROMPT, WORK_CALL, WORK_RESULT, REPLY],
|
|
234
|
+
[
|
|
235
|
+
DEFERRED_LISTING,
|
|
236
|
+
{
|
|
237
|
+
"type": "attachment",
|
|
238
|
+
"attachment": {
|
|
239
|
+
"type": "deferred_tools_delta",
|
|
240
|
+
"removedNames": [DESKTOP_TITLE_TOOL],
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
PROMPT,
|
|
244
|
+
REPLY,
|
|
245
|
+
],
|
|
246
|
+
],
|
|
247
|
+
)
|
|
248
|
+
def test_should_pass_a_session_without_the_title_tool(
|
|
249
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
250
|
+
capsys: pytest.CaptureFixture[str],
|
|
251
|
+
tmp_path: Path,
|
|
252
|
+
all_entries: list[dict[str, object]],
|
|
253
|
+
) -> None:
|
|
254
|
+
assert run_gate(monkeypatch, capsys, tmp_path, all_entries) == (0, "")
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def test_should_pass_when_the_transcript_cannot_be_read(
|
|
258
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
|
|
259
|
+
) -> None:
|
|
260
|
+
hook_input = {"hook_event_name": "Stop", "transcript_path": str(tmp_path / "missing.jsonl")}
|
|
261
|
+
monkeypatch.setattr(
|
|
262
|
+
"sys.stdin", io.TextIOWrapper(io.BytesIO(json.dumps(hook_input).encode("utf-8")))
|
|
263
|
+
)
|
|
264
|
+
assert (session_title_turn_gate.main(), capsys.readouterr().out) == (0, "")
|
|
@@ -11,6 +11,7 @@ This family rewrites a Git Bash command when path conversion would change a revi
|
|
|
11
11
|
- `blocking/cloud_graphql_gate.py` denies the `gh` commands that call GraphQL (`gh api graphql` with any flags before the endpoint, `gh pr`, `gh issue`, `gh project` and `gh discussion` except `gh pr diff` and `gh pr list --web`, `gh status`, `gh org list`, `gh release list`, `gh release view` with a tag, `gh workflow run` without `--ref`, and a few `repo`, `label`, `search`, `ruleset` and `gist` commands, read past `if`, `!` and command substitutions) and an HTTP client or `python -c` style call to `api.github.com/graphql`, with or without a port, inside a cloud session, where the proxy answers GraphQL with HTTP 403, and names the REST routes to run instead.
|
|
12
12
|
- `blocking/bash_post_call_dispatcher.py` runs hosted observers and joins their context output without blocking the call.
|
|
13
13
|
- `advisory/pr_done_reminder.py` adds a pull request checklist after a successful push or pull request creation.
|
|
14
|
+
- `advisory/owned_pr_review_loop.py` adds review-loop instructions after the agent opens a pull request on an owned repository outside the preset list of repositories that run their own automated review: run efficient-review until clean, mark the pull request ready, babysit it, and ship it to merge.
|
|
14
15
|
|
|
15
16
|
## When it fires
|
|
16
17
|
|
|
@@ -21,6 +22,7 @@ This family rewrites a Git Bash command when path conversion would change a revi
|
|
|
21
22
|
- `blocking/cloud_graphql_gate.py` runs inside that dispatcher on `PreToolUse`, matcher `Bash`, timeout `60` seconds, for the Bash and PowerShell tools, when `CLAUDE_CODE_REMOTE` is `true`.
|
|
22
23
|
- `blocking/bash_post_call_dispatcher.py` runs on `PostToolUse`, matcher `Bash|PowerShell`, timeout `60` seconds in `hooks.json`.
|
|
23
24
|
- `advisory/pr_done_reminder.py` runs inside that dispatcher on `PostToolUse`, matcher `Bash|PowerShell`, timeout `60` seconds. `ALL_BASH_POST_TOOL_USE_HOSTED_HOOK_ENTRIES` selects `Bash` and `PowerShell`.
|
|
25
|
+
- `advisory/owned_pr_review_loop.py` runs on `PostToolUse`, matcher `Bash|PowerShell|exec_command|shell_command|mcp__.*__(create_pull_request|github_create_pull_request)`, timeout `10` seconds in `hooks.json`. It wakes on `gh pr create`, `gh pr new`, a POST to `gh api repos/<owner>/<repo>/pulls`, and an MCP pull request creation. The installer also registers it on Codex `PostToolUse` through `ALL_CODEX_NATIVE_POST_TOOL_USE_HOOK_SCRIPT_NAMES`.
|
|
24
26
|
|
|
25
27
|
## Proving it
|
|
26
28
|
|
|
@@ -34,6 +36,7 @@ Preconditions:
|
|
|
34
36
|
- **Cloud GraphQL gate.** Input is a `gh api graphql` command with `CLAUDE_CODE_REMOTE=true`. Run `python -m pytest packages/claude-dev-env/hooks/blocking/test_cloud_graphql_gate.py -q`. The adjacent test observes a denial that names the REST routes, and silence outside a cloud session.
|
|
35
37
|
- **Post-call dispatch.** Input is a Bash call that yields hosted context. Run `python -m pytest packages/claude-dev-env/hooks/blocking/test_bash_post_call_dispatcher.py -q`. The adjacent test observes a joined context payload and exit code zero.
|
|
36
38
|
- **Pull request reminder.** Input is a completed push or pull request creation. Run `python -m pytest packages/claude-dev-env/hooks/advisory/test_pr_done_reminder.py -q`. The adjacent test checks the reminder trigger and checklist verdict.
|
|
39
|
+
- **Owned pull request review loop.** Input is a pull request creation result on an owned repository. Run `python -m pytest packages/claude-dev-env/hooks/advisory/test_owned_pr_review_loop.py -q`. The adjacent test observes the review-loop block for owned repositories and silence for the preset and foreign repositories.
|
|
37
40
|
|
|
38
41
|
## Gotchas
|
|
39
42
|
|
|
@@ -12,6 +12,7 @@ This family stops a chat reply, edited message, or tool call when its payload br
|
|
|
12
12
|
- `blocking/edit_marker_gate.py` rejects strikethrough and edit-note markers in replacement chat text or cards.
|
|
13
13
|
- `blocking/issue_close_handoff_gate.py` denies an issue comment or issue write that closes the issue and routes a found defect to another issue or epic.
|
|
14
14
|
- `blocking/session_title_format_gate.py` denies a session title that breaks the `<emoji> <name>` shape or has a name longer than 25 characters.
|
|
15
|
+
- `blocking/session_title_turn_gate.py` holds the end of a turn in a session that has a `__set_session_title` tool until a title call in that turn comes back without an error. A held turn whose title call failed may end.
|
|
15
16
|
- `blocking/step_note_gate.py` requires a status line before a tool call while its opt-in flag is on.
|
|
16
17
|
- `blocking/verify_before_acting.py` blocks a completed mutating call when the agent's reasoning contains an unchecked claim.
|
|
17
18
|
- `blocking/pr_lifecycle_skill_gate.py` denies a commit, push, pull request action, or merge until the `pr-lifecycle` skill appears in the transcript since the last compaction.
|
|
@@ -30,6 +31,7 @@ This family stops a chat reply, edited message, or tool call when its payload br
|
|
|
30
31
|
- `blocking/edit_marker_gate.py` runs on `PreToolUse`, matcher `mcp__.*__update_message`, timeout `10` seconds in `hooks.json`.
|
|
31
32
|
- `blocking/issue_close_handoff_gate.py` runs on `PreToolUse`, matcher `mcp__.*__(add_issue_comment|issue_write|update_issue_comment)`, timeout `10` seconds in `hooks.json`.
|
|
32
33
|
- `blocking/session_title_format_gate.py` runs on `PreToolUse`, matcher `mcp__.*__set_session_title`, timeout `10` seconds in `hooks.json`.
|
|
34
|
+
- `blocking/session_title_turn_gate.py` runs on `Stop`, timeout `10` seconds in `hooks.json`.
|
|
33
35
|
- `blocking/step_note_gate.py` runs on `PreToolUse`, matcher `*`, timeout `15` seconds in `hooks.json`.
|
|
34
36
|
- `blocking/verify_before_acting.py` runs on `PostToolUse`, matcher `Write|Edit|MultiEdit|NotebookEdit|Agent|Task|apply_patch|Bash|PowerShell|mcp__.*`, timeout `10` seconds in `hooks.json`.
|
|
35
37
|
- `blocking/pr_lifecycle_skill_gate.py` runs on `PreToolUse`, matcher `Bash|PowerShell|mcp__.*__(create_pull_request|merge_pull_request|enable_pr_auto_merge|update_pull_request|actions_run_trigger)`, timeout `10` seconds in `hooks.json`.
|
|
@@ -52,6 +54,7 @@ Preconditions:
|
|
|
52
54
|
- **Edit marker.** Input is an update message with strikethrough or an edit note. Run `python -m pytest packages/claude-dev-env/hooks/blocking/test_edit_marker_gate.py -q`. The adjacent test observes a denial; clean replacement text passes.
|
|
53
55
|
- **Handoff close.** Input is an issue comment that says the issue is closed and that the found defect belongs to another issue. Run `python -m pytest packages/claude-dev-env/hooks/blocking/test_issue_close_handoff_gate.py -q`. The adjacent test observes a denial; a close with a plain cross-reference and a routing comment on an open issue both pass.
|
|
54
56
|
- **Session title.** Input is a set session title call with no leading emoji or a name over 25 characters. Run `python -m pytest packages/claude-dev-env/hooks/blocking/test_session_title_format_gate.py -q`. The adjacent test observes a denial that names the broken rule; a well-formed title passes.
|
|
57
|
+
- **Title this turn.** Input is a transcript that lists the title tool and a turn with no title call. Run `python -m pytest packages/claude-dev-env/hooks/blocking/test_session_title_turn_gate.py -q`. The adjacent test observes a `block` decision that names the tool and the four status emoji; a turn whose title call came back without an error passes.
|
|
55
58
|
- **Step note.** Input is a tool call after a transcript message without a status line while the flag is on. Run `python -m pytest packages/claude-dev-env/hooks/blocking/test_step_note_gate.py -q`. The adjacent test observes a block until a status line appears.
|
|
56
59
|
- **Checked claim.** Input is a Write after hedged reasoning in the transcript. Run `python -m pytest packages/claude-dev-env/hooks/blocking/test_verify_before_acting.py -q`. The adjacent test observes a block and a logged blocked outcome.
|
|
57
60
|
- **Lifecycle skill.** Input is a `git commit` or `gh pr create` command with no `pr-lifecycle` invocation in the transcript. Run `python -m pytest packages/claude-dev-env/hooks/blocking/test_pr_lifecycle_skill_gate.py -q`. The adjacent test observes a denial, then a silent allow once the skill or its slash command appears.
|
|
@@ -10,7 +10,6 @@ This family injects reminders and task guidance when an agent starts, resumes, s
|
|
|
10
10
|
- `session/working_style_prompt.py` injects the session's working style at session start.
|
|
11
11
|
- `session/advisor_rules_prompt.py` injects advisor guidance when the built-in advisor is enabled.
|
|
12
12
|
- `session/orchestrator_auto_starter.py` injects an orchestrator directive when its environment flag is enabled.
|
|
13
|
-
- `session/deepen_first_prompt.py` rewrites a first interactive user task so the worker does the omitted expert job.
|
|
14
13
|
- `session/artifact_template_pointer.py` routes an agent that starts an artifact page by scope. A status or progress report prefers the status flowchart from `docs/templates/artifact-page/status-flowchart.html`, unless the person asks for another shape. One flow or one comparison is an inline SVG flowchart with minimal text. A multi-part plan or evidence is an html-plan page from `docs/templates/html-plan/README.md`. When the request names the older artifact page template, it points at `docs/templates/artifact-page/README.md`, which names the tracker template and the diagram template and which ask each fits.
|
|
15
14
|
- `advisory/auto_mode_denial_quick_fix.py` proposes one `autoMode.allow` entry and a PowerShell block that writes it after an auto mode denial. It never retries the denied call.
|
|
16
15
|
|
|
@@ -19,8 +18,6 @@ This family injects reminders and task guidance when an agent starts, resumes, s
|
|
|
19
18
|
- `session/skill_loaded_reminder.py` runs on `PreToolUse` matcher `Agent|Task` at `10` seconds, `UserPromptSubmit` matcher empty at `10` seconds, `SessionStart` matcher `compact` at `10` seconds, and `SubagentStart` matcher `workflow-subagent` at `10` seconds in `hooks.json`.
|
|
20
19
|
- `session/compaction_recall_prompt.py` runs on `SessionStart` matcher `compact` at `10` seconds in `hooks.json`. The condensing hook is a `type: "agent"` hook on `PostToolUse` matcher `Bash`, with `if` set to `Bash(*compaction_user_messages.py*)`, model `haiku`, and a `180` second timeout. The hook's Read call obeys session permissions, so the package `settings.json` allows `Read(~/.claude/compaction-recall/**)`.
|
|
21
20
|
- `session/task_tool_prompt.py`, `session/working_style_prompt.py`, `session/advisor_rules_prompt.py`, and `session/orchestrator_auto_starter.py` run on `SessionStart`, matcher empty, timeout `10` seconds each in `hooks.json`.
|
|
22
|
-
- `session/deepen_first_prompt.py` runs on `UserPromptSubmit`, matcher empty, timeout `10` seconds in `hooks.json`.
|
|
23
|
-
- In Cursor, `session/deepen_first_prompt.py` also runs on `beforeSubmitPrompt`. Cursor passes that event name to the `UserPromptSubmit` copy it imports from the Claude Code settings too. When both copies run, `session/deepen_turn_marker.py` creates the turn marker only when it does not exist yet, so one copy injects. A later prompt skips when either the transcript marker or the session marker exists, or when the transcript already has a user line in Claude Code's or Cursor's shape.
|
|
24
21
|
- `session/artifact_template_pointer.py` runs on `PreToolUse` matcher `Artifact|Skill` at `10` seconds in `hooks.json`. It speaks only on an `Artifact` call with action `quickstart` or a `Skill` call that loads `artifact-design`.
|
|
25
22
|
- `advisory/auto_mode_denial_quick_fix.py` runs on `PermissionDenied`, matcher `*`, timeout `10` seconds in `hooks.json`.
|
|
26
23
|
|
|
@@ -36,7 +33,6 @@ Preconditions:
|
|
|
36
33
|
- **Working style.** Input is a SessionStart event. Run `python -m pytest packages/claude-dev-env/hooks/session/test_working_style_prompt.py -q`. The adjacent test observes the fixed context text.
|
|
37
34
|
- **Advisor guidance.** Input is SessionStart with `advisorModel` set. Run `python -m pytest packages/claude-dev-env/hooks/session/test_advisor_rules_prompt.py -q`. The adjacent test observes guidance only when the advisor setting is enabled.
|
|
38
35
|
- **Orchestrator start.** Input is SessionStart with its opt-in flag set. Run `python -m pytest packages/claude-dev-env/hooks/session/test_orchestrator_auto_starter.py -q`. The adjacent test observes an orchestrator directive.
|
|
39
|
-
- **First-task deepen.** Input is a first UserPromptSubmit task. Run `python -m pytest packages/claude-dev-env/hooks/session/test_deepen_first_prompt.py -q`. The adjacent test observes a skip table, a refusal of the paste path, a rewrite that carries the SCOPE, EXAMPLES, and ZOOM-IN BLOCKER lines, and one injection when two copies see the same Cursor prompt. `python -m pytest packages/claude-dev-env/hooks/session/test_deepen_turn_marker.py -q` observes that only the first marker claim wins. With nothing configured, `deepen_default_rewrite.py` builds that role-shaped rewrite, and `test_deepen_default_rewrite.py` checks the gate accepts it.
|
|
40
36
|
- **Artifact template pointer.** Input is an `Artifact` quickstart call. Run `python -m pytest packages/claude-dev-env/hooks/session/test_artifact_template_pointer.py -q`. The adjacent test observes the status flowchart path first, the html-plan path ahead of the template path in `additionalContext`, and silence on a publish call or another skill.
|
|
41
37
|
- **Denial quick fix.** Input is a `PermissionDenied` event with a bracketed rule label and a classifier verdict. Run `python -m pytest packages/claude-dev-env/hooks/advisory/test_auto_mode_denial_quick_fix.py -q`. The adjacent test observes the named rule, the allow entry, and the PowerShell block; a denial with no verdict gets a note and no block.
|
|
42
38
|
|
package/hooks/hooks.json
CHANGED
|
@@ -284,11 +284,6 @@
|
|
|
284
284
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/skill_loaded_reminder.py",
|
|
285
285
|
"timeout": 10
|
|
286
286
|
},
|
|
287
|
-
{
|
|
288
|
-
"type": "command",
|
|
289
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/deepen_first_prompt.py",
|
|
290
|
-
"timeout": 10
|
|
291
|
-
},
|
|
292
287
|
{
|
|
293
288
|
"type": "command",
|
|
294
289
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/lifecycle/nested_project_hooks.py",
|
|
@@ -389,6 +384,11 @@
|
|
|
389
384
|
"type": "command",
|
|
390
385
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/open_tasks_stop_gate.py",
|
|
391
386
|
"timeout": 10
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
"type": "command",
|
|
390
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/session_title_turn_gate.py",
|
|
391
|
+
"timeout": 10
|
|
392
392
|
}
|
|
393
393
|
]
|
|
394
394
|
}
|
|
@@ -456,6 +456,16 @@
|
|
|
456
456
|
}
|
|
457
457
|
]
|
|
458
458
|
},
|
|
459
|
+
{
|
|
460
|
+
"matcher": "Bash|PowerShell|exec_command|shell_command|mcp__.*__(create_pull_request|github_create_pull_request)",
|
|
461
|
+
"hooks": [
|
|
462
|
+
{
|
|
463
|
+
"type": "command",
|
|
464
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/advisory/owned_pr_review_loop.py",
|
|
465
|
+
"timeout": 10
|
|
466
|
+
}
|
|
467
|
+
]
|
|
468
|
+
},
|
|
459
469
|
{
|
|
460
470
|
"matcher": "Bash",
|
|
461
471
|
"hooks": [
|
|
@@ -14,7 +14,6 @@ __all__ = [
|
|
|
14
14
|
"ENABLED_PLUGINS_SETTINGS_KEY",
|
|
15
15
|
"PLUGIN_MARKETPLACE_SEPARATOR",
|
|
16
16
|
"PROJECT_SETTINGS_DIRECTORY_NAME",
|
|
17
|
-
"DEEPEN_FIRST_PROMPT_PLUGIN_NAME",
|
|
18
17
|
"SESSION_PROMPTS_PLUGIN_NAME",
|
|
19
18
|
"SHELL_GUARDS_PLUGIN_NAME",
|
|
20
19
|
"USER_SETTINGS_FILE_NAME",
|
|
@@ -24,7 +23,6 @@ ENABLED_PLUGINS_SETTINGS_KEY = "enabledPlugins"
|
|
|
24
23
|
PLUGIN_MARKETPLACE_SEPARATOR = "@"
|
|
25
24
|
SHELL_GUARDS_PLUGIN_NAME = "shell-guards"
|
|
26
25
|
SESSION_PROMPTS_PLUGIN_NAME = "session-prompts"
|
|
27
|
-
DEEPEN_FIRST_PROMPT_PLUGIN_NAME = "deepen-first-prompt"
|
|
28
26
|
CLAUDE_CONFIG_DIR_ENV_VAR = "CLAUDE_CONFIG_DIR"
|
|
29
27
|
CLAUDE_PROJECT_DIR_ENV_VAR = "CLAUDE_PROJECT_DIR"
|
|
30
28
|
DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME = ".claude"
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Constants for the owned pull request review-loop context hook.
|
|
2
|
+
|
|
3
|
+
The hook is a PostToolUse observer. It never blocks. After the agent opens a
|
|
4
|
+
pull request on a repository we own, outside the preset list of repositories
|
|
5
|
+
that run their own automated review, it tells the agent to own an efficient
|
|
6
|
+
review loop and drive the pull request through to merge.
|
|
7
|
+
|
|
8
|
+
Private owner and repository names stay out of this public tree. Each set holds the
|
|
9
|
+
SHA-256 digest of a name normalized to lower-case ASCII letters and digits,
|
|
10
|
+
the same form ``scripts/private_terms.py`` uses::
|
|
11
|
+
|
|
12
|
+
"Acme-Widgets" -> "acmewidgets"
|
|
13
|
+
"Acme-Widgets/tools" -> "acmewidgetstools"
|
|
14
|
+
|
|
15
|
+
To add an owner or a preset repository, append the digest of its normalized
|
|
16
|
+
form.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"NAME_DIGEST_TEXT_ENCODING",
|
|
25
|
+
"ALL_OWNED_OWNER_DIGESTS",
|
|
26
|
+
"ALL_PRESET_REPOSITORY_DIGESTS",
|
|
27
|
+
"ALL_SHELL_TOOL_NAMES",
|
|
28
|
+
"ALL_SHELL_COMMAND_INPUT_KEYS",
|
|
29
|
+
"SHELL_ARGUMENT_SEPARATOR",
|
|
30
|
+
"ALL_GH_PR_CREATE_ACTIONS",
|
|
31
|
+
"GH_PROGRAM_NAME",
|
|
32
|
+
"GH_PR_SUBCOMMAND",
|
|
33
|
+
"GH_API_SUBCOMMAND",
|
|
34
|
+
"GH_API_PULLS_ENDPOINT_PATTERN",
|
|
35
|
+
"ALL_GH_API_BODY_OPTIONS",
|
|
36
|
+
"ALL_GH_API_METHOD_OPTIONS",
|
|
37
|
+
"GH_API_POST_METHOD",
|
|
38
|
+
"GH_LONG_OPTION_PREFIX",
|
|
39
|
+
"GH_SHORT_OPTION_LENGTH",
|
|
40
|
+
"ALL_GH_API_SHORT_OPTIONS",
|
|
41
|
+
"GH_OPTION_VALUE_SEPARATOR",
|
|
42
|
+
"MCP_TOOL_NAME_PREFIX",
|
|
43
|
+
"ALL_MCP_CREATE_PULL_REQUEST_TOOL_SUFFIXES",
|
|
44
|
+
"MCP_OWNER_INPUT_KEY",
|
|
45
|
+
"MCP_REPOSITORY_INPUT_KEY",
|
|
46
|
+
"PULL_REQUEST_URL_PATTERN",
|
|
47
|
+
"ALREADY_EXISTS_MARKER",
|
|
48
|
+
"ALL_DECLARED_URL_KEYS",
|
|
49
|
+
"JSON_OBJECT_OPENER",
|
|
50
|
+
"REVIEW_LOOP_CONTEXT_TEMPLATE",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
NAME_DIGEST_TEXT_ENCODING: str = "utf-8"
|
|
54
|
+
ALL_OWNED_OWNER_DIGESTS: frozenset[str] = frozenset(
|
|
55
|
+
{
|
|
56
|
+
"2e8b9d88060d471690c122e6843f3bec8c95116d643af2f5501e0881b10e83c4",
|
|
57
|
+
"3a92fc8d256dc1f50dc8794573f80de695516f0f0e5c235118bd0c18be3d4434",
|
|
58
|
+
"d526db5593d5986500e4770cb6018f2d33c459ed93657487958a6086e52e6147",
|
|
59
|
+
"95d4d8808440e130d0ee753e0b7530eba4534dd41a7320ad8b710a44d40441f4",
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
ALL_PRESET_REPOSITORY_DIGESTS: frozenset[str] = frozenset(
|
|
63
|
+
{"d362bee4d80f5474921b917c127f2b0680c0a5261cb352e59de35cc87b00989f"}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
ALL_SHELL_TOOL_NAMES: frozenset[str] = frozenset(
|
|
67
|
+
{"Bash", "PowerShell", "exec_command", "shell_command"}
|
|
68
|
+
)
|
|
69
|
+
ALL_SHELL_COMMAND_INPUT_KEYS: tuple[str, ...] = ("command", "cmd")
|
|
70
|
+
SHELL_ARGUMENT_SEPARATOR: str = " "
|
|
71
|
+
GH_PROGRAM_NAME: str = "gh"
|
|
72
|
+
GH_PR_SUBCOMMAND: str = "pr"
|
|
73
|
+
ALL_GH_PR_CREATE_ACTIONS: frozenset[str] = frozenset({"create", "new"})
|
|
74
|
+
GH_API_SUBCOMMAND: str = "api"
|
|
75
|
+
GH_API_PULLS_ENDPOINT_PATTERN: re.Pattern[str] = re.compile(r"/?repos/[^/\s]+/[^/\s]+/pulls")
|
|
76
|
+
ALL_GH_API_BODY_OPTIONS: frozenset[str] = frozenset(
|
|
77
|
+
{"-f", "-F", "--field", "--raw-field", "--input"}
|
|
78
|
+
)
|
|
79
|
+
ALL_GH_API_METHOD_OPTIONS: frozenset[str] = frozenset({"-X", "--method"})
|
|
80
|
+
GH_API_POST_METHOD: str = "POST"
|
|
81
|
+
GH_LONG_OPTION_PREFIX: str = "--"
|
|
82
|
+
GH_SHORT_OPTION_LENGTH: int = 2
|
|
83
|
+
ALL_GH_API_SHORT_OPTIONS: frozenset[str] = frozenset({"-f", "-F", "-X"})
|
|
84
|
+
GH_OPTION_VALUE_SEPARATOR: str = "="
|
|
85
|
+
|
|
86
|
+
MCP_TOOL_NAME_PREFIX: str = "mcp__"
|
|
87
|
+
ALL_MCP_CREATE_PULL_REQUEST_TOOL_SUFFIXES: tuple[str, ...] = (
|
|
88
|
+
"__create_pull_request",
|
|
89
|
+
"__github_create_pull_request",
|
|
90
|
+
)
|
|
91
|
+
MCP_OWNER_INPUT_KEY: str = "owner"
|
|
92
|
+
MCP_REPOSITORY_INPUT_KEY: str = "repo"
|
|
93
|
+
|
|
94
|
+
PULL_REQUEST_URL_PATTERN: re.Pattern[str] = re.compile(
|
|
95
|
+
r"https://github\.com/(?P<owner>[A-Za-z0-9][A-Za-z0-9-]*)/"
|
|
96
|
+
r"(?P<repository>[A-Za-z0-9._-]+)/pull/(?P<number>\d+)"
|
|
97
|
+
)
|
|
98
|
+
ALREADY_EXISTS_MARKER: str = "already exists"
|
|
99
|
+
ALL_DECLARED_URL_KEYS: tuple[str, ...] = ("html_url", "url")
|
|
100
|
+
JSON_OBJECT_OPENER: str = "{"
|
|
101
|
+
|
|
102
|
+
REVIEW_LOOP_CONTEXT_TEMPLATE: str = (
|
|
103
|
+
"=== OWNED PR REVIEW LOOP (context reminder, never a block) ===\n"
|
|
104
|
+
"You opened {owner}/{repository}#{number} {url}\n"
|
|
105
|
+
"This repository has no automated review, so you own the review and the merge.\n"
|
|
106
|
+
"1. Review loop. Load the efficient-review skill (/efficient-review). Review the\n"
|
|
107
|
+
" branch, check each finding against the code, fix every one that holds, push,\n"
|
|
108
|
+
" and review again. Repeat until a pass reports no findings.\n"
|
|
109
|
+
"2. Mark ready. Once the loop is clean, CI is green, and no review thread is open,\n"
|
|
110
|
+
" mark the PR ready for review (gh pr ready {number} --repo {owner}/{repository},\n"
|
|
111
|
+
" or update_pull_request with draft false).\n"
|
|
112
|
+
"3. Babysit. Follow the pstack babysit playbook (poteto-mode/playbooks/babysit.md,\n"
|
|
113
|
+
" or the standalone babysit skill outside poteto-mode). Fix failing CI, resolve\n"
|
|
114
|
+
" merge conflicts, and answer review threads until the PR is green and mergeable.\n"
|
|
115
|
+
" Any new code goes back through step 1 before the next push.\n"
|
|
116
|
+
"4. Ship. Follow the pstack shipping playbook (poteto-mode/playbooks/shipping.md)\n"
|
|
117
|
+
" to land the PR, or the pr-lifecycle skill outside poteto-mode. Read the PR\n"
|
|
118
|
+
" back and confirm it reads merged.\n"
|
|
119
|
+
"Run each step yourself. The task ends at a merged PR or a named blocker."
|
|
120
|
+
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Constants for session_title_format_gate.py."""
|
|
1
|
+
"""Constants for session_title_format_gate.py and session_title_turn_gate.py."""
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
|
|
@@ -10,14 +10,15 @@ FORMAT_GATE_EVENT_NAME = "PreToolUse"
|
|
|
10
10
|
ALLOW_EXIT_CODE = 0
|
|
11
11
|
BLOCK_EXIT_CODE = 2
|
|
12
12
|
|
|
13
|
-
STATUS_PREFIX_PATTERN = re.compile("^(\U0001f6a9|\u2705|\u23f3)\ufe0f? (?=\\S)")
|
|
13
|
+
STATUS_PREFIX_PATTERN = re.compile("^(\U0001f6a9|\U0001f6a7|\u2705|\u23f3)\ufe0f? (?=\\S)")
|
|
14
14
|
MAXIMUM_NAME_LENGTH = 25
|
|
15
15
|
TRAILING_PUNCTUATION = ".!?,;:"
|
|
16
16
|
IDENTIFIER_DIGITS_PATTERN = re.compile(r"\d{3,}")
|
|
17
17
|
|
|
18
18
|
STATUS_PREFIX_MESSAGE = (
|
|
19
19
|
"The title must start with one status emoji and one space:"
|
|
20
|
-
" \U0001f6a9 when the user must act, \
|
|
20
|
+
" \U0001f6a9 when the user must act, \U0001f6a7 when an outside dependency"
|
|
21
|
+
" blocks the work, \u2705 when the work is done,"
|
|
21
22
|
" \u23f3 while work runs and nothing waits on the user."
|
|
22
23
|
)
|
|
23
24
|
SINGLE_LINE_MESSAGE = "The title must be one line."
|
|
@@ -31,3 +32,40 @@ FORMAT_RETRY_INSTRUCTION = (
|
|
|
31
32
|
" the main change or outcome in concrete nouns, such as"
|
|
32
33
|
" '\u23f3 Broker gate + replay rule'."
|
|
33
34
|
)
|
|
35
|
+
|
|
36
|
+
TURN_GATE_EVENT_NAME = "Stop"
|
|
37
|
+
TRANSCRIPT_PATH_KEY = "transcript_path"
|
|
38
|
+
STOP_HOOK_ACTIVE_KEY = "stop_hook_active"
|
|
39
|
+
DECISION_KEY = "decision"
|
|
40
|
+
REASON_KEY = "reason"
|
|
41
|
+
BLOCK_DECISION = "block"
|
|
42
|
+
UTF8_ENCODING = "utf-8"
|
|
43
|
+
DECODE_ERRORS_POLICY = "replace"
|
|
44
|
+
ENTRY_TYPE_KEY = "type"
|
|
45
|
+
USER_ENTRY_TYPE = "user"
|
|
46
|
+
ASSISTANT_ENTRY_TYPE = "assistant"
|
|
47
|
+
ATTACHMENT_KEY = "attachment"
|
|
48
|
+
MESSAGE_KEY = "message"
|
|
49
|
+
CONTENT_KEY = "content"
|
|
50
|
+
TOOL_USE_BLOCK_TYPE = "tool_use"
|
|
51
|
+
TOOL_RESULT_BLOCK_TYPE = "tool_result"
|
|
52
|
+
BLOCK_ID_KEY = "id"
|
|
53
|
+
BLOCK_NAME_KEY = "name"
|
|
54
|
+
TOOL_USE_ID_KEY = "tool_use_id"
|
|
55
|
+
IS_ERROR_KEY = "is_error"
|
|
56
|
+
SIDECHAIN_FLAG_KEY = "isSidechain"
|
|
57
|
+
ALL_SKIPPED_PROMPT_FLAGS = ("isMeta", SIDECHAIN_FLAG_KEY, "isCompactSummary")
|
|
58
|
+
ALL_TOOL_LISTING_NAME_KEYS = ("addedNames", "readdedNames")
|
|
59
|
+
ALL_TOOL_LISTING_RECORD_KEYS = ("tools", "entries")
|
|
60
|
+
REMOVED_NAMES_KEY = "removedNames"
|
|
61
|
+
TITLE_TOOL_DISPLAY_SEPARATOR = " or "
|
|
62
|
+
|
|
63
|
+
TURN_BLOCK_REASON = (
|
|
64
|
+
"This turn set no session title. Call {tool_name} now with '<emoji> <name>':"
|
|
65
|
+
" \U0001f6a9 the user must act, \U0001f6a7 an outside dependency blocks the work,"
|
|
66
|
+
" \u2705 the work is done, \u23f3 work runs and nothing waits on the user."
|
|
67
|
+
" When the tool is deferred, load its schema with ToolSearch first."
|
|
68
|
+
" When it asks for a session id, get it from get_session with no session_id."
|
|
69
|
+
" After the call, end the turn with one short closing sentence."
|
|
70
|
+
" Leave the earlier reply as it stands and do not repeat it."
|
|
71
|
+
)
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
Claude Code reads project hooks from ``<project>/.claude/settings.json`` of the
|
|
5
5
|
directory a session starts in. A cloud session that holds several repository
|
|
6
6
|
checkouts starts in their parent directory, which has no settings of its own,
|
|
7
|
-
and ``--add-dir`` loads no hooks from the checkouts. Their start hooks,
|
|
8
|
-
|
|
7
|
+
and ``--add-dir`` loads no hooks from the checkouts. Their start hooks, prompt
|
|
8
|
+
hooks, and tool guards then never run. This hook runs them::
|
|
9
9
|
|
|
10
10
|
session directory (no .git, no .claude/settings.json)
|
|
11
11
|
|-- checkout-a/.claude/settings.json hooks run with CLAUDE_PROJECT_DIR=checkout-a
|
|
@@ -362,7 +362,7 @@ def test_should_carry_child_user_prompt_submit_output_as_additional_context(
|
|
|
362
362
|
context_output = {
|
|
363
363
|
"hookSpecificOutput": {
|
|
364
364
|
"hookEventName": "UserPromptSubmit",
|
|
365
|
-
"additionalContext": "
|
|
365
|
+
"additionalContext": "CHILD_CONTEXT:v1\nTreat the submitted prompt as this rewritten task.",
|
|
366
366
|
}
|
|
367
367
|
}
|
|
368
368
|
_make_checkout(
|
|
@@ -382,7 +382,7 @@ def test_should_carry_child_user_prompt_submit_output_as_additional_context(
|
|
|
382
382
|
assert completed_process.returncode == 0
|
|
383
383
|
hook_output = json.loads(completed_process.stdout)
|
|
384
384
|
assert hook_output["hookSpecificOutput"]["hookEventName"] == "UserPromptSubmit"
|
|
385
|
-
assert "
|
|
385
|
+
assert "CHILD_CONTEXT:v1" in hook_output["hookSpecificOutput"]["additionalContext"]
|
|
386
386
|
|
|
387
387
|
|
|
388
388
|
def test_should_stay_quiet_on_an_event_it_does_not_forward(
|
|
@@ -168,8 +168,8 @@ def typed_user_messages(all_transcript_lines: Iterable[str]) -> list[TypedMessag
|
|
|
168
168
|
::
|
|
169
169
|
|
|
170
170
|
user "fix the login bug" -> kept
|
|
171
|
-
user "<command-name>/
|
|
172
|
-
<command-args>speed it up</command-args>" -> "/
|
|
171
|
+
user "<command-name>/deepstack-mode</command-name>
|
|
172
|
+
<command-args>speed it up</command-args>" -> "/deepstack-mode speed it up"
|
|
173
173
|
user tool_result, isMeta skill body,
|
|
174
174
|
isCompactSummary, isSidechain, origin peer -> left out
|
|
175
175
|
user "<system-reminder>...", "<local-command-stdout>" -> left out
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-dev-env",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.90.0",
|
|
4
4
|
"description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"cde": "bin/cde.mjs"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"test": "node --test --test-concurrency=
|
|
12
|
+
"test": "node --test --test-concurrency=4 \"bin/*.test.mjs\" \".agents/skills/**/*.test.mjs\" \"tests/fresh-session/**/*.test.mjs\" \"scripts/ci/*.test.mjs\""
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"bin/",
|