claude-dev-env 8.65.0 → 8.66.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/grok-spawn/SKILL.md +5 -54
- package/.agents/skills/grok-spawn/reference/batch-spec.md +49 -0
- package/.agents/skills/orchestrator/test_orchestrator_skill_contract.py +0 -2
- package/.agents/skills/pull-request/SKILL.md +5 -49
- package/.agents/skills/pull-request/reference/examples.md +13 -0
- package/.agents/skills/pull-request/reference/legacy-author-recovery.md +11 -0
- package/bin/install.settings-defaults.test.mjs +1 -1
- package/docs/references/working-style.md +40 -0
- package/hooks/blocking/artifact_dark_mode_gate.py +184 -0
- package/hooks/blocking/artifact_dark_mode_render.cjs +132 -0
- package/hooks/blocking/test_artifact_dark_mode_gate.py +114 -0
- package/hooks/features/conduct-gates.md +4 -0
- package/hooks/hooks.json +10 -0
- package/hooks/hooks_constants/artifact_dark_mode_gate_constants.py +57 -0
- package/hooks/hooks_constants/test_artifact_dark_mode_gate_constants.py +17 -0
- package/hooks/hooks_constants/working_style_prompt_constants.py +6 -35
- package/hooks/session/test_working_style_prompt.py +19 -32
- package/hooks/session/working_style_prompt.py +3 -6
- package/package.json +1 -1
- package/scripts/policy_lint/adapter_configuration.py +4 -0
- package/scripts/policy_lint/config/constants.py +1 -0
- package/scripts/tests/test_adapter_configuration.py +8 -0
- package/scripts/tests/test_rule_load_scopes.py +0 -2
- package/settings.json +2 -1
- package/.agents/skills/.claude/CLAUDE.md +0 -1
- package/.agents/skills/AGENTS.md +0 -1
- package/.agents/skills/_shared/.claude/CLAUDE.md +0 -1
- package/.agents/skills/_shared/AGENTS.md +0 -1
- package/.agents/skills/_shared/advisor/.claude/CLAUDE.md +0 -1
- package/.agents/skills/_shared/advisor/AGENTS.md +0 -1
- package/.agents/skills/everything-search/.claude/CLAUDE.md +0 -1
- package/.agents/skills/everything-search/AGENTS.md +0 -1
- package/.agents/skills/fresh-branch/.claude/CLAUDE.md +0 -1
- package/.agents/skills/fresh-branch/AGENTS.md +0 -1
- package/.agents/skills/grok-spawn/.claude/CLAUDE.md +0 -1
- package/.agents/skills/grok-spawn/AGENTS.md +0 -1
- package/.agents/skills/orchestrator/.claude/CLAUDE.md +0 -1
- package/.agents/skills/orchestrator/AGENTS.md +0 -1
- package/.agents/skills/orchestrator/reference/.claude/CLAUDE.md +0 -1
- package/.agents/skills/orchestrator/reference/AGENTS.md +0 -1
- package/.agents/skills/second-account-workers/.claude/CLAUDE.md +0 -1
- package/.agents/skills/second-account-workers/AGENTS.md +0 -1
- package/.agents/skills/skill-builder/.claude/CLAUDE.md +0 -1
- package/.agents/skills/skill-builder/AGENTS.md +0 -1
- package/.agents/skills/task-build/.claude/CLAUDE.md +0 -1
- package/.agents/skills/task-build/AGENTS.md +0 -1
- package/.agents/skills-archived/AGENTS.md +0 -1
- package/.agents/skills-archived/anthropic-plan/.claude/CLAUDE.md +0 -1
- package/.agents/skills-archived/anthropic-plan/AGENTS.md +0 -1
- package/.agents/skills-archived/anthropic-plan/scripts/.claude/CLAUDE.md +0 -1
- package/.agents/skills-archived/anthropic-plan/scripts/AGENTS.md +0 -1
- package/.agents/skills-archived/anthropic-plan/scripts/anthropic_plan_scripts_constants/.claude/CLAUDE.md +0 -1
- package/.agents/skills-archived/anthropic-plan/scripts/anthropic_plan_scripts_constants/AGENTS.md +0 -1
- package/.agents/skills-archived/anthropic-plan/templates/.claude/CLAUDE.md +0 -1
- package/.agents/skills-archived/anthropic-plan/templates/AGENTS.md +0 -1
- package/.agents/skills-archived/anthropic-plan/workflow/.claude/CLAUDE.md +0 -1
- package/.agents/skills-archived/anthropic-plan/workflow/AGENTS.md +0 -1
- package/.agents/skills-archived/auditing-claude-config/.claude/CLAUDE.md +0 -1
- package/.agents/skills-archived/auditing-claude-config/AGENTS.md +0 -1
- package/.agents/skills-archived/bugteam/.claude/CLAUDE.md +0 -1
- package/.agents/skills-archived/bugteam/AGENTS.md +0 -1
- package/.agents/skills-archived/bugteam/reference/.claude/CLAUDE.md +0 -1
- package/.agents/skills-archived/bugteam/reference/AGENTS.md +0 -1
- package/.agents/skills-archived/bugteam/reference/obstacles/.claude/CLAUDE.md +0 -1
- package/.agents/skills-archived/bugteam/reference/obstacles/AGENTS.md +0 -1
- package/.agents/skills-archived/bugteam/scripts/.claude/CLAUDE.md +0 -1
- package/.agents/skills-archived/bugteam/scripts/AGENTS.md +0 -1
- package/.agents/skills-archived/bugteam/scripts/bugteam_scripts_constants/.claude/CLAUDE.md +0 -1
- package/.agents/skills-archived/bugteam/scripts/bugteam_scripts_constants/AGENTS.md +0 -1
- package/.agents/skills-archived/codex-review/.claude/CLAUDE.md +0 -1
- package/.agents/skills-archived/codex-review/AGENTS.md +0 -1
- package/.agents/skills-archived/codex-review/reference/.claude/CLAUDE.md +0 -1
- package/.agents/skills-archived/codex-review/reference/AGENTS.md +0 -1
- package/.agents/skills-archived/codex-review/scripts/codex_review_scripts_constants/.claude/CLAUDE.md +0 -1
- package/.agents/skills-archived/codex-review/scripts/codex_review_scripts_constants/AGENTS.md +0 -1
- package/.agents/skills-archived/copilot-review/.claude/CLAUDE.md +0 -1
- package/.agents/skills-archived/copilot-review/AGENTS.md +0 -1
- package/.agents/skills-archived/pr-converge/.claude/CLAUDE.md +0 -1
- package/.agents/skills-archived/pr-converge/AGENTS.md +0 -1
- package/.agents/skills-archived/pr-converge/pr_converge_skill_constants/.claude/CLAUDE.md +0 -1
- package/.agents/skills-archived/pr-converge/pr_converge_skill_constants/AGENTS.md +0 -1
- package/.agents/skills-archived/pr-converge/reference/.claude/CLAUDE.md +0 -1
- package/.agents/skills-archived/pr-converge/reference/AGENTS.md +0 -1
- package/.agents/skills-archived/pr-converge/reference/obstacles/.claude/CLAUDE.md +0 -1
- package/.agents/skills-archived/pr-converge/reference/obstacles/AGENTS.md +0 -1
- package/.agents/skills-archived/pr-converge/scripts/.claude/CLAUDE.md +0 -1
- package/.agents/skills-archived/pr-converge/scripts/AGENTS.md +0 -1
- package/.agents/skills-archived/pr-converge/scripts/pr_converge_scripts_constants/.claude/CLAUDE.md +0 -1
- package/.agents/skills-archived/pr-converge/scripts/pr_converge_scripts_constants/AGENTS.md +0 -1
- package/.agents/skills-archived/pr-converge/workflows/.claude/CLAUDE.md +0 -1
- package/.agents/skills-archived/pr-converge/workflows/AGENTS.md +0 -1
- package/.agents/skills-archived/recall/.claude/CLAUDE.md +0 -1
- package/.agents/skills-archived/recall/AGENTS.md +0 -1
- package/.agents/skills-archived/remember/.claude/CLAUDE.md +0 -1
- package/.agents/skills-archived/remember/AGENTS.md +0 -1
- package/_shared/AGENTS.md +0 -1
- package/_shared/CLAUDE.md +0 -1
- package/_shared/advisor/AGENTS.md +0 -1
- package/_shared/advisor/CLAUDE.md +0 -1
- package/_shared/pr-loop/scripts/AGENTS.md +0 -1
- package/_shared/pr-loop/scripts/CLAUDE.md +0 -1
- package/_shared/pr-loop/scripts/code_rules_gate_parts/AGENTS.md +0 -1
- package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +0 -1
- package/_shared/pr-loop/scripts/codex_review_scripts_constants/.claude/CLAUDE.md +0 -1
- package/_shared/pr-loop/scripts/codex_review_scripts_constants/AGENTS.md +0 -1
- package/_shared/pr-loop/scripts/pr_converge_scripts_constants/.claude/CLAUDE.md +0 -1
- package/_shared/pr-loop/scripts/pr_converge_scripts_constants/AGENTS.md +0 -1
- package/_shared/pr-loop/scripts/pr_converge_skill_constants/.claude/CLAUDE.md +0 -1
- package/_shared/pr-loop/scripts/pr_converge_skill_constants/AGENTS.md +0 -1
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/AGENTS.md +0 -1
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +0 -1
- package/_shared/pr-loop/scripts/tests/AGENTS.md +0 -1
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +0 -1
- package/_shared/process-tree/AGENTS.md +0 -1
- package/_shared/process-tree/CLAUDE.md +0 -1
- package/audit-rubrics/AGENTS.md +0 -1
- package/audit-rubrics/CLAUDE.md +0 -1
- package/audit-rubrics/category_rubrics/AGENTS.md +0 -1
- package/audit-rubrics/category_rubrics/CLAUDE.md +0 -1
- package/audit-rubrics/prompts/AGENTS.md +0 -1
- package/audit-rubrics/prompts/CLAUDE.md +0 -1
- package/bin/AGENTS.md +0 -1
- package/bin/CLAUDE.md +0 -1
- package/commands/AGENTS.md +0 -1
- package/commands/CLAUDE.md +0 -1
- package/docs/AGENTS.md +0 -1
- package/docs/CLAUDE.md +0 -1
- package/docs/references/AGENTS.md +0 -1
- package/docs/references/CLAUDE.md +0 -1
- package/hooks/advisory/AGENTS.md +0 -1
- package/hooks/advisory/CLAUDE.md +0 -1
- package/hooks/blocking/AGENTS.md +0 -1
- package/hooks/blocking/CLAUDE.md +0 -1
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/AGENTS.md +0 -1
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +0 -1
- package/hooks/blocking/config/AGENTS.md +0 -1
- package/hooks/blocking/config/CLAUDE.md +0 -1
- package/hooks/blocking/inventory_intent_records/AGENTS.md +0 -1
- package/hooks/blocking/inventory_intent_records/CLAUDE.md +0 -1
- package/hooks/blocking/package_inventory_stale_blocker_parts/AGENTS.md +0 -1
- package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +0 -1
- package/hooks/blocking/pii_prevention_blocker_parts/AGENTS.md +0 -1
- package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +0 -1
- package/hooks/blocking/tdd_enforcer_parts/AGENTS.md +0 -1
- package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +0 -1
- package/hooks/git-hooks/AGENTS.md +0 -1
- package/hooks/git-hooks/CLAUDE.md +0 -1
- package/hooks/git-hooks/git_hooks_constants/AGENTS.md +0 -1
- package/hooks/git-hooks/git_hooks_constants/CLAUDE.md +0 -1
- package/hooks/hooks_constants/AGENTS.md +0 -1
- package/hooks/hooks_constants/CLAUDE.md +0 -1
- package/hooks/lifecycle/AGENTS.md +0 -1
- package/hooks/lifecycle/CLAUDE.md +0 -1
- package/hooks/observability/AGENTS.md +0 -1
- package/hooks/observability/CLAUDE.md +0 -1
- package/hooks/session/AGENTS.md +0 -1
- package/hooks/session/CLAUDE.md +0 -1
- package/hooks/validation/AGENTS.md +0 -1
- package/hooks/validation/CLAUDE.md +0 -1
- package/hooks/validators/AGENTS.md +0 -1
- package/hooks/validators/CLAUDE.md +0 -1
- package/hooks/workflow/AGENTS.md +0 -1
- package/hooks/workflow/CLAUDE.md +0 -1
- package/rules/AGENTS.md +0 -1
- package/rules/CLAUDE.md +0 -1
- package/scripts/AGENTS.md +0 -1
- package/scripts/CLAUDE.md +0 -1
- package/scripts/dev_env_scripts_constants/AGENTS.md +0 -1
- package/scripts/dev_env_scripts_constants/CLAUDE.md +0 -1
- package/scripts/tests/AGENTS.md +0 -1
- package/scripts/tests/CLAUDE.md +0 -1
- package/system-prompts/AGENTS.md +0 -1
- package/system-prompts/CLAUDE.md +0 -1
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Behavior tests for the artifact dark mode publish gate."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
14
|
+
|
|
15
|
+
from blocking.artifact_dark_mode_gate import (
|
|
16
|
+
render_findings,
|
|
17
|
+
format_block_reason,
|
|
18
|
+
published_page_path,
|
|
19
|
+
wrap_in_publish_skeleton,
|
|
20
|
+
)
|
|
21
|
+
from hooks_constants.artifact_dark_mode_gate_constants import (
|
|
22
|
+
ALLOW_EXIT_CODE,
|
|
23
|
+
BLOCK_EXIT_CODE,
|
|
24
|
+
PUBLISH_SKELETON_PREFIX,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
GATE_SCRIPT_PATH = Path(__file__).resolve().parent / "artifact_dark_mode_gate.py"
|
|
28
|
+
THEME_COLOR_STYLES = (
|
|
29
|
+
"<style>:root{--paper:#faf9f5;--ink:#141413;color-scheme:light dark}"
|
|
30
|
+
"@media (prefers-color-scheme: dark){:root:not([data-theme=light]){--paper:#262624;--ink:#faf9f5}}"
|
|
31
|
+
":root[data-theme=dark]{--paper:#262624;--ink:#faf9f5}"
|
|
32
|
+
"html{background:var(--paper);color:var(--ink)}h1{color:var(--ink)}</style>"
|
|
33
|
+
)
|
|
34
|
+
PAGE_WITH_TOKENS_ON_HTML_ONLY = THEME_COLOR_STYLES + "<h1>Weekly totals</h1><p>Three runs today.</p>"
|
|
35
|
+
PAGE_WITH_THEMED_BODY = (
|
|
36
|
+
THEME_COLOR_STYLES + "<style>body{background:var(--paper);color:var(--ink)}</style>"
|
|
37
|
+
"<h1>Weekly totals</h1><p>Three runs today.</p>"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _renderer_available() -> bool:
|
|
42
|
+
if shutil.which("node") is None:
|
|
43
|
+
return False
|
|
44
|
+
findings_by_signal, _ = render_findings("<p>probe</p>")
|
|
45
|
+
return findings_by_signal is not None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
needs_renderer = pytest.mark.skipif(
|
|
49
|
+
not _renderer_available(), reason="node Playwright with Chromium is not installed"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _run_gate(tool_input: dict[str, object]) -> subprocess.CompletedProcess[str]:
|
|
54
|
+
return subprocess.run(
|
|
55
|
+
[sys.executable, str(GATE_SCRIPT_PATH)],
|
|
56
|
+
input=json.dumps({"tool_name": "Artifact", "tool_input": tool_input}),
|
|
57
|
+
capture_output=True,
|
|
58
|
+
text=True,
|
|
59
|
+
check=False,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_should_return_the_page_path_for_an_html_publish() -> None:
|
|
64
|
+
assert published_page_path("Artifact", {"file_path": "/tmp/page.html"}) == Path(
|
|
65
|
+
"/tmp/page.html"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_should_skip_reads_assets_typed_creates_and_other_tools() -> None:
|
|
70
|
+
assert published_page_path("Artifact", {"action": "read", "url": "x"}) is None
|
|
71
|
+
assert published_page_path("Artifact", {"file_path": "/tmp/logo.html", "asset": True}) is None
|
|
72
|
+
assert published_page_path("Artifact", {"file_path": "/tmp/data.html", "type_url": "x"}) is None
|
|
73
|
+
assert published_page_path("Artifact", {"file_path": "/tmp/notes.md"}) is None
|
|
74
|
+
assert published_page_path("Write", {"file_path": "/tmp/page.html"}) is None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_should_put_the_page_after_the_skeleton_that_pins_body_light() -> None:
|
|
78
|
+
wrapped_page = wrap_in_publish_skeleton("<p>x</p>")
|
|
79
|
+
assert wrapped_page == PUBLISH_SKELETON_PREFIX + "<p>x</p>"
|
|
80
|
+
assert "background:#faf9f5" in wrapped_page
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_should_list_each_finding_under_its_dark_signal() -> None:
|
|
84
|
+
block_reason = format_block_reason(
|
|
85
|
+
{"prefers-color-scheme: dark": ['text "A" has contrast 1.00:1']}
|
|
86
|
+
)
|
|
87
|
+
assert "Under prefers-color-scheme: dark:" in block_reason
|
|
88
|
+
assert '- text "A" has contrast 1.00:1' in block_reason
|
|
89
|
+
assert "body{background:var(--paper);color:var(--ink)}" in block_reason
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@needs_renderer
|
|
93
|
+
def test_should_fail_a_page_that_themes_html_but_not_body() -> None:
|
|
94
|
+
findings_by_signal, _ = render_findings(PAGE_WITH_TOKENS_ON_HTML_ONLY)
|
|
95
|
+
assert set(findings_by_signal) == {"prefers-color-scheme: dark", 'data-theme="dark"'}
|
|
96
|
+
assert any(
|
|
97
|
+
"Weekly totals" in each_finding for each_finding in findings_by_signal['data-theme="dark"']
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@needs_renderer
|
|
102
|
+
def test_should_pass_a_page_that_themes_body() -> None:
|
|
103
|
+
assert render_findings(PAGE_WITH_THEMED_BODY) == ({}, "")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@needs_renderer
|
|
107
|
+
def test_should_deny_and_then_allow_the_publish_call(tmp_path: Path) -> None:
|
|
108
|
+
page_path = tmp_path / "report.html"
|
|
109
|
+
page_path.write_text(PAGE_WITH_TOKENS_ON_HTML_ONLY, encoding="utf-8")
|
|
110
|
+
denied_run = _run_gate({"action": "publish", "file_path": str(page_path)})
|
|
111
|
+
assert denied_run.returncode == BLOCK_EXIT_CODE
|
|
112
|
+
assert "has unreadable text" in denied_run.stderr
|
|
113
|
+
page_path.write_text(PAGE_WITH_THEMED_BODY, encoding="utf-8")
|
|
114
|
+
assert _run_gate({"file_path": str(page_path)}).returncode == ALLOW_EXIT_CODE
|
|
@@ -12,6 +12,7 @@ This family stops a chat reply, edited message, or tool call when its payload br
|
|
|
12
12
|
- `blocking/step_note_gate.py` requires a status line before a tool call while its opt-in flag is on.
|
|
13
13
|
- `blocking/verify_before_acting.py` blocks a completed mutating call when the agent's reasoning contains an unchecked claim.
|
|
14
14
|
- `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.
|
|
15
|
+
- `blocking/artifact_dark_mode_gate.py` denies an Artifact page publish when the page, inside the publish wrapper, keeps a light body or shows text under 4.5:1 contrast in dark mode. `blocking/artifact_dark_mode_render.cjs` renders the page in headless Chromium for it.
|
|
15
16
|
|
|
16
17
|
## When it fires
|
|
17
18
|
|
|
@@ -23,6 +24,7 @@ This family stops a chat reply, edited message, or tool call when its payload br
|
|
|
23
24
|
- `blocking/step_note_gate.py` runs on `PreToolUse`, matcher `*`, timeout `15` seconds in `hooks.json`.
|
|
24
25
|
- `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`.
|
|
25
26
|
- `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)`, timeout `10` seconds in `hooks.json`.
|
|
27
|
+
- `blocking/artifact_dark_mode_gate.py` runs on `PreToolUse`, matcher `Artifact`, timeout `60` seconds in `hooks.json`.
|
|
26
28
|
|
|
27
29
|
## Proving it
|
|
28
30
|
|
|
@@ -38,6 +40,7 @@ Preconditions:
|
|
|
38
40
|
- **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.
|
|
39
41
|
- **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.
|
|
40
42
|
- **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.
|
|
43
|
+
- **Dark mode page.** Input is an Artifact publish of a page that themes `html` but leaves `body` to the publish wrapper. Run `python -m pytest packages/claude-dev-env/hooks/blocking/test_artifact_dark_mode_gate.py -q`. The adjacent test observes a denial that names the text and its contrast; a page that themes `body` passes. The render tests skip on a host with no Node Playwright and Chromium.
|
|
41
44
|
|
|
42
45
|
## Gotchas
|
|
43
46
|
|
|
@@ -47,3 +50,4 @@ Preconditions:
|
|
|
47
50
|
- The edit marker gate scans replacement cards as well as the message text.
|
|
48
51
|
- The handoff close gate reads the `body` and `comment` fields of issue tools only. A `gh issue close --comment` command in Bash and a pull request body that closes the issue with `Closes #N` do not reach it.
|
|
49
52
|
- The lifecycle gate counts only invocations after the last compaction, so a compacted session invokes `pr-lifecycle` again. A missing or unreadable transcript allows the call.
|
|
53
|
+
- The dark mode gate denies a page publish when Node, Playwright or Chromium is missing, and its reason names the install command. It checks both dark signals, `prefers-color-scheme: dark` and `data-theme="dark"` on the root element.
|
package/hooks/hooks.json
CHANGED
|
@@ -152,6 +152,16 @@
|
|
|
152
152
|
}
|
|
153
153
|
]
|
|
154
154
|
},
|
|
155
|
+
{
|
|
156
|
+
"matcher": "Artifact",
|
|
157
|
+
"hooks": [
|
|
158
|
+
{
|
|
159
|
+
"type": "command",
|
|
160
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/artifact_dark_mode_gate.py",
|
|
161
|
+
"timeout": 60
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
},
|
|
155
165
|
{
|
|
156
166
|
"matcher": "Artifact|Skill",
|
|
157
167
|
"hooks": [
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Names, limits and messages for the artifact_dark_mode_gate PreToolUse hook."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"ALLOW_EXIT_CODE",
|
|
9
|
+
"ALL_SKIPPED_PUBLISH_INPUT_KEYS",
|
|
10
|
+
"ARTIFACT_ACTION_INPUT_KEY",
|
|
11
|
+
"ARTIFACT_FILE_PATH_INPUT_KEY",
|
|
12
|
+
"ARTIFACT_PUBLISH_ACTION",
|
|
13
|
+
"ARTIFACT_TOOL_NAME",
|
|
14
|
+
"BLOCK_EXIT_CODE",
|
|
15
|
+
"DARK_MODE_FAILURE_HEADER",
|
|
16
|
+
"GATE_EVENT_NAME",
|
|
17
|
+
"HTML_FILE_SUFFIX",
|
|
18
|
+
"PUBLISH_SKELETON_PREFIX",
|
|
19
|
+
"RENDERER_MISSING_EXIT_CODE",
|
|
20
|
+
"RENDERER_MISSING_HEADER",
|
|
21
|
+
"RENDERER_SCRIPT_PATH",
|
|
22
|
+
"RENDERER_TIMEOUT_SECONDS",
|
|
23
|
+
"RETRY_INSTRUCTION",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
ALLOW_EXIT_CODE = 0
|
|
27
|
+
BLOCK_EXIT_CODE = 2
|
|
28
|
+
RENDERER_MISSING_EXIT_CODE = 3
|
|
29
|
+
RENDERER_TIMEOUT_SECONDS = 50
|
|
30
|
+
ARTIFACT_TOOL_NAME = "Artifact"
|
|
31
|
+
ARTIFACT_ACTION_INPUT_KEY = "action"
|
|
32
|
+
ARTIFACT_PUBLISH_ACTION = "publish"
|
|
33
|
+
ARTIFACT_FILE_PATH_INPUT_KEY = "file_path"
|
|
34
|
+
ALL_SKIPPED_PUBLISH_INPUT_KEYS = ("asset", "type_url")
|
|
35
|
+
HTML_FILE_SUFFIX = ".html"
|
|
36
|
+
GATE_EVENT_NAME = "PreToolUse"
|
|
37
|
+
RENDERER_SCRIPT_PATH = (
|
|
38
|
+
Path(__file__).resolve().parent.parent / "blocking" / "artifact_dark_mode_render.cjs"
|
|
39
|
+
)
|
|
40
|
+
PUBLISH_SKELETON_PREFIX = (
|
|
41
|
+
"<!doctype html><html><head><meta charset=utf8>"
|
|
42
|
+
'<meta name=viewport content="width=device-width,initial-scale=1">'
|
|
43
|
+
"<style>:root{color-scheme:light}body{margin:0;padding:0;"
|
|
44
|
+
"font:14px -apple-system,BlinkMacSystemFont,sans-serif;"
|
|
45
|
+
"background:#faf9f5;color:#141413}img{max-width:100%}"
|
|
46
|
+
"[hidden]:not([hidden=until-found i]){display:none!important}</style>"
|
|
47
|
+
"</head><body>\n"
|
|
48
|
+
)
|
|
49
|
+
DARK_MODE_FAILURE_HEADER = (
|
|
50
|
+
"Inside the artifact publish wrapper, this page has unreadable text when the viewer uses dark mode.\n"
|
|
51
|
+
)
|
|
52
|
+
RENDERER_MISSING_HEADER = "The page did not render for its contrast check before publish: "
|
|
53
|
+
RETRY_INSTRUCTION = (
|
|
54
|
+
"\nThe publish wrapper pins body to a light background and dark text. Set body background and"
|
|
55
|
+
" color from the page's theme tokens, for example body{background:var(--paper);color:var(--ink)},"
|
|
56
|
+
' so both flip under prefers-color-scheme: dark and data-theme="dark". Then publish again.'
|
|
57
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Behavioral tests for the artifact page gate's paths and publish wrapper."""
|
|
2
|
+
|
|
3
|
+
from hooks_constants.artifact_dark_mode_gate_constants import (
|
|
4
|
+
PUBLISH_SKELETON_PREFIX,
|
|
5
|
+
RENDERER_SCRIPT_PATH,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_renderer_path_names_the_packaged_render_script() -> None:
|
|
10
|
+
assert RENDERER_SCRIPT_PATH.is_file()
|
|
11
|
+
assert RENDERER_SCRIPT_PATH.suffix == ".cjs"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_publish_wrapper_pins_body_light_and_opens_the_body() -> None:
|
|
15
|
+
assert "body{margin:0;padding:0;" in PUBLISH_SKELETON_PREFIX
|
|
16
|
+
assert "background:#faf9f5;color:#141413" in PUBLISH_SKELETON_PREFIX
|
|
17
|
+
assert PUBLISH_SKELETON_PREFIX.endswith("<body>\n")
|
|
@@ -3,43 +3,14 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
__all__ = [
|
|
6
|
+
"WORKING_STYLE_GUIDE_RELATIVE_PATH",
|
|
6
7
|
"WORKING_STYLE_PROMPT",
|
|
7
8
|
]
|
|
8
9
|
|
|
10
|
+
WORKING_STYLE_GUIDE_RELATIVE_PATH = "docs/references/working-style.md"
|
|
11
|
+
|
|
9
12
|
WORKING_STYLE_PROMPT = (
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"continuity, and sharing when a user-facing response needs that presentation. "
|
|
14
|
-
"Keep responses focused, brief, and concise. Apply ~/.claude/rules/asd-ste100-language.md for "
|
|
15
|
-
"user-facing word choice, sentence style, tone, punctuation, and prose form. "
|
|
16
|
-
"Keep disclaimers and caveats short while giving "
|
|
17
|
-
"the main answer most of the response. Give a high-level explanation by default "
|
|
18
|
-
"and provide depth when the request calls for it. "
|
|
19
|
-
"On a typed request, state your next action in one sentence before the first "
|
|
20
|
-
"tool call. "
|
|
21
|
-
"Finish with the outcome in the first sentence. "
|
|
22
|
-
"Send the user only what they must act on or need to know. When a background "
|
|
23
|
-
"event, such as a task notification, an agent message, or a scheduled wake, "
|
|
24
|
-
"starts a turn and nothing in it needs the user, end the turn with no text. A "
|
|
25
|
-
"report that a fix is done carries three lines: the fix acknowledged, what "
|
|
26
|
-
"changed, and the proof that it works. Put the rest of the detail in the pull "
|
|
27
|
-
"request or a linked file. "
|
|
28
|
-
"Match written-document length to the task. Cover the substance and keep every "
|
|
29
|
-
"section, summary, and phrase useful. "
|
|
30
|
-
"Deliver the requested work at its intended scope. Make routine judgment calls "
|
|
31
|
-
"yourself. Ask for direction when different interpretations would produce "
|
|
32
|
-
"materially different work. When a request seems mistaken or a better approach "
|
|
33
|
-
"exists, state the concern briefly and continue with the requested task. "
|
|
34
|
-
"A request to remove something is complete once it is gone. Put nothing in its "
|
|
35
|
-
"place, and pass a replacement idea to the requester as a question. Finish "
|
|
36
|
-
"the complete task and keep actions within the requested scope. "
|
|
37
|
-
"When a request has multiple reasonable interpretations, state your understanding "
|
|
38
|
-
"and the assumptions that shape the work. Ask one focused clarification question "
|
|
39
|
-
"when the ambiguity changes the outcome, scope, audience, format, or risk. Use a "
|
|
40
|
-
"clearly stated low-risk assumption when the intended result remains stable. "
|
|
41
|
-
"Pause for the user's choice before making a high-impact decision. "
|
|
42
|
-
"Use current, immediately relevant context. "
|
|
43
|
-
"Use full terms and specific names for repository work. Keep all "
|
|
44
|
-
"text concise, clear, direct, and useful."
|
|
13
|
+
f"Read ~/.claude/{WORKING_STYLE_GUIDE_RELATIVE_PATH} before your first reply. "
|
|
14
|
+
"It sets how you keep task records, present answers, reply, hold scope, and "
|
|
15
|
+
"ask questions."
|
|
45
16
|
)
|
|
@@ -15,9 +15,12 @@ for each_sys_path_entry in (str(_SESSION_DIR), str(_HOOKS_ROOT)):
|
|
|
15
15
|
import working_style_prompt as starter
|
|
16
16
|
|
|
17
17
|
from hooks_constants.working_style_prompt_constants import (
|
|
18
|
+
WORKING_STYLE_GUIDE_RELATIVE_PATH,
|
|
18
19
|
WORKING_STYLE_PROMPT,
|
|
19
20
|
)
|
|
20
21
|
|
|
22
|
+
WORKING_STYLE_GUIDE_PATH = _HOOKS_ROOT.parent / WORKING_STYLE_GUIDE_RELATIVE_PATH
|
|
23
|
+
|
|
21
24
|
|
|
22
25
|
def _run_main() -> str:
|
|
23
26
|
"""Return stdout produced by running the hook's main()."""
|
|
@@ -38,38 +41,22 @@ class TestWorkingStylePrompt:
|
|
|
38
41
|
emitted = json.loads(_run_main())
|
|
39
42
|
assert emitted["hookSpecificOutput"]["additionalContext"] == WORKING_STYLE_PROMPT
|
|
40
43
|
|
|
41
|
-
def
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
assert
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
assert "
|
|
51
|
-
assert "
|
|
52
|
-
assert "
|
|
53
|
-
assert "
|
|
54
|
-
assert "
|
|
55
|
-
assert "
|
|
56
|
-
assert "Pause for the user's choice before making a high-impact decision." in
|
|
57
|
-
|
|
58
|
-
def test_prompt_limits_replies_to_what_the_user_must_act_on_or_know(self) -> None:
|
|
59
|
-
assert "Send the user only what they must act on or need to know." in WORKING_STYLE_PROMPT
|
|
60
|
-
assert "starts a turn and nothing in it needs the user, end the turn with no text." in WORKING_STYLE_PROMPT
|
|
61
|
-
assert "On a typed request, state your next action in one sentence" in WORKING_STYLE_PROMPT
|
|
62
|
-
assert "Before your first tool call, state your next action" not in WORKING_STYLE_PROMPT
|
|
63
|
-
assert (
|
|
64
|
-
"A report that a fix is done carries three lines: the fix acknowledged, "
|
|
65
|
-
"what changed, and the proof that it works."
|
|
66
|
-
) in WORKING_STYLE_PROMPT
|
|
67
|
-
assert "Put the rest of the detail in the pull request or a linked file." in WORKING_STYLE_PROMPT
|
|
68
|
-
|
|
69
|
-
def test_prompt_drops_the_clauses_that_invite_surplus_detail(self) -> None:
|
|
70
|
-
assert "supporting detail for readers who want it" not in WORKING_STYLE_PROMPT
|
|
71
|
-
assert "give brief updates when you find important information" not in WORKING_STYLE_PROMPT
|
|
72
|
-
assert "Name each action, fact, reason, and outcome." not in WORKING_STYLE_PROMPT
|
|
44
|
+
def test_prompt_points_at_the_working_style_guide(self) -> None:
|
|
45
|
+
assert f"~/.claude/{WORKING_STYLE_GUIDE_RELATIVE_PATH}" in WORKING_STYLE_PROMPT
|
|
46
|
+
assert WORKING_STYLE_GUIDE_PATH.is_file()
|
|
47
|
+
|
|
48
|
+
def test_prompt_stays_a_short_pointer(self) -> None:
|
|
49
|
+
assert len(WORKING_STYLE_PROMPT) < len(WORKING_STYLE_GUIDE_PATH.read_text(encoding="utf-8")) // 10
|
|
50
|
+
|
|
51
|
+
def test_guide_carries_the_policy_and_scope_guidance(self) -> None:
|
|
52
|
+
guide_text = WORKING_STYLE_GUIDE_PATH.read_text(encoding="utf-8")
|
|
53
|
+
assert "Document each task in a location that remains easy to find later." in guide_text
|
|
54
|
+
assert "Deliver the requested work at its intended scope." in guide_text
|
|
55
|
+
assert "A request to remove something is complete once it is gone." in guide_text
|
|
56
|
+
assert "Send the user only what they must act on or need to know." in guide_text
|
|
57
|
+
assert "starts a turn and nothing in it needs the user, end the turn with no text." in guide_text
|
|
58
|
+
assert "On a typed request, state your next action in one sentence" in guide_text
|
|
59
|
+
assert "Pause for the user's choice before making a high-impact decision." in guide_text
|
|
73
60
|
|
|
74
61
|
def test_build_session_directive_returns_the_shared_constant(self) -> None:
|
|
75
62
|
assert starter.build_session_directive() == WORKING_STYLE_PROMPT
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""SessionStart hook — inject working-style guidance into the session.
|
|
3
3
|
|
|
4
|
-
At session start this hook emits an ``additionalContext``
|
|
5
|
-
working-style
|
|
6
|
-
|
|
7
|
-
ledger, narrate before the first tool call on a typed request, lead with the
|
|
8
|
-
outcome, send only what the user must act on or know, stay silent on a
|
|
9
|
-
background-event turn that needs nothing, and stay at the asked scope.
|
|
4
|
+
At session start this hook emits an ``additionalContext`` line that points the
|
|
5
|
+
session at ``docs/references/working-style.md``, the guide for task records,
|
|
6
|
+
presentation, replies, scope, and questions.
|
|
10
7
|
The hook writes nothing and runs no tools itself.
|
|
11
8
|
"""
|
|
12
9
|
|
package/package.json
CHANGED
|
@@ -135,6 +135,7 @@ def _names_exempt_registration_path(registered_string: str) -> bool:
|
|
|
135
135
|
hooks/blocking/verify_before_acting.py -> exempt
|
|
136
136
|
hooks/blocking/session_title_format_gate.py -> exempt
|
|
137
137
|
hooks/blocking/session_title_stop_gate.py -> exempt
|
|
138
|
+
hooks/blocking/artifact_dark_mode_gate.py -> exempt
|
|
138
139
|
hooks/blocking/some_new_blocker.py -> flagged
|
|
139
140
|
|
|
140
141
|
The Bash PreToolUse dispatcher sits under ``blocking/`` for layout reasons
|
|
@@ -163,6 +164,9 @@ def _names_exempt_registration_path(registered_string: str) -> bool:
|
|
|
163
164
|
denies a malformed title, and the other asks for a title before a turn
|
|
164
165
|
ends. They decide the shape of a title and no code or safety policy.
|
|
165
166
|
|
|
167
|
+
The artifact page gate denies a page publish whose text is unreadable in
|
|
168
|
+
dark mode. It decides how a page renders and no code or safety policy.
|
|
169
|
+
|
|
166
170
|
Args:
|
|
167
171
|
registered_string: One command, path, script, or entrypoint string.
|
|
168
172
|
"""
|
|
@@ -71,6 +71,7 @@ ALL_ACTION_BOUNDARY_EXEMPT_REGISTRATION_PATHS = (
|
|
|
71
71
|
"blocking/verify_before_acting.py",
|
|
72
72
|
"blocking/session_title_format_gate.py",
|
|
73
73
|
"blocking/session_title_stop_gate.py",
|
|
74
|
+
"blocking/artifact_dark_mode_gate.py",
|
|
74
75
|
)
|
|
75
76
|
ALL_TEST_DIRECTORY_NAMES = frozenset({"tests"})
|
|
76
77
|
ALL_TEST_FILE_PREFIXES = ("test_",)
|
|
@@ -109,6 +109,14 @@ def test_new_session_title_gate_registrations_are_clean(tmp_path: Path) -> None:
|
|
|
109
109
|
assert adapters.hook_configuration_diagnostics(current_document, tmp_path) == ()
|
|
110
110
|
|
|
111
111
|
|
|
112
|
+
def test_new_artifact_page_gate_registration_is_clean(tmp_path: Path) -> None:
|
|
113
|
+
current_document = _hook_document(
|
|
114
|
+
["hooks/blocking/artifact_dark_mode_gate.py"],
|
|
115
|
+
'{"hooks": {}}',
|
|
116
|
+
)
|
|
117
|
+
assert adapters.hook_configuration_diagnostics(current_document, tmp_path) == ()
|
|
118
|
+
|
|
119
|
+
|
|
112
120
|
def test_new_blocker_in_staged_change_is_rejected(tmp_path: Path) -> None:
|
|
113
121
|
current_document = _hook_document(
|
|
114
122
|
["hooks/blocking/new.py"],
|
package/settings.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@../AGENTS.md
|
package/.agents/skills/AGENTS.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@../AGENTS.md
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@../AGENTS.md
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@../AGENTS.md
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@../AGENTS.md
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@../AGENTS.md
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@../AGENTS.md
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@../AGENTS.md
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@../AGENTS.md
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@../AGENTS.md
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@../AGENTS.md
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@../AGENTS.md
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@../AGENTS.md
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@../AGENTS.md
|
package/.agents/skills-archived/anthropic-plan/scripts/anthropic_plan_scripts_constants/AGENTS.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@../AGENTS.md
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@../AGENTS.md
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@../AGENTS.md
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@../AGENTS.md
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@../AGENTS.md
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@../AGENTS.md
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@../AGENTS.md
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@../AGENTS.md
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@../AGENTS.md
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@../AGENTS.md
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@../AGENTS.md
|
package/.agents/skills-archived/codex-review/scripts/codex_review_scripts_constants/AGENTS.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|