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
|
@@ -108,54 +108,10 @@ Flag sets and profile meaning:
|
|
|
108
108
|
|
|
109
109
|
### 3. Write the batch JSON spec
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
"role": "bugteam",
|
|
116
|
-
"should_ping": false,
|
|
117
|
-
"workers": [
|
|
118
|
-
{
|
|
119
|
-
"role_name": "investigate-hooks",
|
|
120
|
-
"prompt_parts": [
|
|
121
|
-
"/abs/path/to/readonly-brief.md",
|
|
122
|
-
"/abs/path/to/task-body.md",
|
|
123
|
-
"/abs/path/to/report-contract.md"
|
|
124
|
-
],
|
|
125
|
-
"cwd": "/abs/path/to/worktree",
|
|
126
|
-
"tool_profile": "readonly",
|
|
127
|
-
"timeout_seconds": 600,
|
|
128
|
-
"is_repo_only": true,
|
|
129
|
-
"agent_name": "poteto-agent"
|
|
130
|
-
}
|
|
131
|
-
]
|
|
132
|
-
}
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
| Field | Meaning |
|
|
136
|
-
|---|---|
|
|
137
|
-
| `role` | Preflight role configuration to check (default `bugteam`) |
|
|
138
|
-
| `should_ping` | When true, preflight runs the opt-in live ping |
|
|
139
|
-
| `workers` | Non-empty list of worker objects |
|
|
140
|
-
| `role_name` | Label on the summary report for this worker |
|
|
141
|
-
| `prompt_parts` | Ordered absolute paths to part files |
|
|
142
|
-
| `cwd` | Working directory for that worker |
|
|
143
|
-
| `tool_profile` | `readonly` or `build` |
|
|
144
|
-
| `timeout_seconds` | Per-worker timeout (default 600, ceiling 5400). A spec asking for more is refused |
|
|
145
|
-
| `is_repo_only` | Readonly only: when true, also pass `--disable-web-search` |
|
|
146
|
-
| `agent_name` | Use `poteto-agent`; prompts carry the worker role. |
|
|
147
|
-
|
|
148
|
-
Workers run with no turn cap. The timeout is the only bound on a worker's
|
|
149
|
-
length, and a worker that hits it is killed with its whole process tree and
|
|
150
|
-
reported as `timeout`.
|
|
151
|
-
|
|
152
|
-
A worker entry accepts these fields and no others. Any other key fails the
|
|
153
|
-
load with an error naming the stray key and listing the accepted set, so a
|
|
154
|
-
misspelled field shows up at startup rather than passing for a setting that
|
|
155
|
-
took effect.
|
|
156
|
-
|
|
157
|
-
Put the spec file under the run state directory (or any path you pass to
|
|
158
|
-
`--spec`).
|
|
111
|
+
Write the spec from the shape and field table in
|
|
112
|
+
[`reference/batch-spec.md`](reference/batch-spec.md). A worker entry accepts
|
|
113
|
+
only the listed fields. Put the spec file under the run state directory (or any
|
|
114
|
+
path you pass to `--spec`).
|
|
159
115
|
|
|
160
116
|
### 4. Launch the batch
|
|
161
117
|
|
|
@@ -222,14 +178,9 @@ Build workers stop at stage-ready edits and a written report. They do not run
|
|
|
222
178
|
| File | Purpose |
|
|
223
179
|
|---|---|
|
|
224
180
|
| `SKILL.md` | Trigger, gotchas, process, batch shape, composition |
|
|
225
|
-
| `CLAUDE.md` | Package map for this skill folder |
|
|
226
181
|
| `reference/worker-briefs.md` | Readonly brief, build brief, report contract templates |
|
|
227
182
|
| `reference/flag-profiles.md` | Readonly vs build flags, shared flags, socket and stagger rules |
|
|
228
|
-
|
|
229
|
-
## Folder map
|
|
230
|
-
|
|
231
|
-
- `SKILL.md` — hub and process.
|
|
232
|
-
- `reference/` — brief templates and flag profiles.
|
|
183
|
+
| `reference/batch-spec.md` | Batch JSON spec shape and worker fields |
|
|
233
184
|
|
|
234
185
|
## Worker advisors
|
|
235
186
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Batch JSON spec
|
|
2
|
+
|
|
3
|
+
Shape:
|
|
4
|
+
|
|
5
|
+
```json
|
|
6
|
+
{
|
|
7
|
+
"role": "bugteam",
|
|
8
|
+
"should_ping": false,
|
|
9
|
+
"workers": [
|
|
10
|
+
{
|
|
11
|
+
"role_name": "investigate-hooks",
|
|
12
|
+
"prompt_parts": [
|
|
13
|
+
"/abs/path/to/readonly-brief.md",
|
|
14
|
+
"/abs/path/to/task-body.md",
|
|
15
|
+
"/abs/path/to/report-contract.md"
|
|
16
|
+
],
|
|
17
|
+
"cwd": "/abs/path/to/worktree",
|
|
18
|
+
"tool_profile": "readonly",
|
|
19
|
+
"timeout_seconds": 600,
|
|
20
|
+
"is_repo_only": true,
|
|
21
|
+
"agent_name": "poteto-agent"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
| Field | Meaning |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `role` | Preflight role configuration to check (default `bugteam`) |
|
|
30
|
+
| `should_ping` | When true, preflight runs the opt-in live ping |
|
|
31
|
+
| `workers` | Non-empty list of worker objects |
|
|
32
|
+
| `role_name` | Label on the summary report for this worker |
|
|
33
|
+
| `prompt_parts` | Ordered absolute paths to part files |
|
|
34
|
+
| `cwd` | Working directory for that worker |
|
|
35
|
+
| `tool_profile` | `readonly` or `build` |
|
|
36
|
+
| `timeout_seconds` | Per-worker timeout (default 600, ceiling 5400). A spec asking for more is refused |
|
|
37
|
+
| `is_repo_only` | Readonly only: when true, also pass `--disable-web-search` |
|
|
38
|
+
| `agent_name` | Use `poteto-agent`; prompts carry the worker role. |
|
|
39
|
+
|
|
40
|
+
Workers run with no turn cap. The timeout is the only bound on a worker's
|
|
41
|
+
length, and a worker that hits it is killed with its whole process tree and
|
|
42
|
+
reported as `timeout`.
|
|
43
|
+
|
|
44
|
+
A worker entry accepts these fields and no others. Any other key fails the
|
|
45
|
+
load with an error naming the stray key and listing the accepted set, so a
|
|
46
|
+
misspelled field fails at startup.
|
|
47
|
+
|
|
48
|
+
Put the spec file under the run state directory (or any path you pass to
|
|
49
|
+
`--spec`).
|
|
@@ -7,11 +7,9 @@ _SKILL_DIR = Path(__file__).resolve().parent
|
|
|
7
7
|
_REFRESH_PATH = _SKILL_DIR.parent / "orchestrator-refresh" / "SKILL.md"
|
|
8
8
|
_ORCHESTRATOR_PATHS = (
|
|
9
9
|
_SKILL_DIR / "SKILL.md",
|
|
10
|
-
_SKILL_DIR / "AGENTS.md",
|
|
11
10
|
_SKILL_DIR / "reference" / "consult-the-orchestrator.md",
|
|
12
11
|
_SKILL_DIR / "reference" / "executor-consult-block.md",
|
|
13
12
|
_SKILL_DIR / "reference" / "host-detect.md",
|
|
14
|
-
_SKILL_DIR / "reference" / "AGENTS.md",
|
|
15
13
|
_REFRESH_PATH,
|
|
16
14
|
)
|
|
17
15
|
_FOREIGN_MARKERS = (
|
|
@@ -8,21 +8,6 @@ description: >-
|
|
|
8
8
|
|
|
9
9
|
# Pull request
|
|
10
10
|
|
|
11
|
-
## Contents
|
|
12
|
-
|
|
13
|
-
- [Principle](#principle)
|
|
14
|
-
- [When this applies](#when-this-applies)
|
|
15
|
-
- [Constraints](#constraints)
|
|
16
|
-
- [Dependencies](#dependencies)
|
|
17
|
-
- [Sub-skills](#sub-skills)
|
|
18
|
-
- [Task seeding](#task-seeding)
|
|
19
|
-
- [Process](#process)
|
|
20
|
-
- [Exit handling](#exit-handling)
|
|
21
|
-
- [Examples](#examples)
|
|
22
|
-
- [Gotchas](#gotchas)
|
|
23
|
-
- [File index](#file-index)
|
|
24
|
-
- [Folder map](#folder-map)
|
|
25
|
-
|
|
26
11
|
## Principle
|
|
27
12
|
|
|
28
13
|
Publish one GitHub pull request action from validated local files and a
|
|
@@ -151,12 +136,8 @@ failure. Never print the value.
|
|
|
151
136
|
|
|
152
137
|
### 6. Recover a selected legacy record
|
|
153
138
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
state file. The command checks the record's age, secure file metadata, and
|
|
157
|
-
contents. The confirmation flag records that the caller verified inactivity.
|
|
158
|
-
Leave every other record untouched. Delete the selected record only after a
|
|
159
|
-
successful restore. Mark this step `N/A` when no legacy state file is selected.
|
|
139
|
+
Follow `reference/legacy-author-recovery.md` only when the user selects one
|
|
140
|
+
legacy state file. Mark this step `N/A` otherwise.
|
|
160
141
|
|
|
161
142
|
### 7. Run the action
|
|
162
143
|
|
|
@@ -185,24 +166,12 @@ only after remote readback proves the requested state.
|
|
|
185
166
|
|
|
186
167
|
## Examples
|
|
187
168
|
|
|
188
|
-
|
|
189
|
-
`pr-create`. `pull_request.py create` publishes one ready pull request. The
|
|
190
|
-
readback matches the title, body, head SHA, and draft state.
|
|
191
|
-
|
|
192
|
-
Rejected comment example: the comment body names a worktree file. The linter
|
|
193
|
-
returns a non-zero exit. No account lookup or GitHub request runs. Replace the
|
|
194
|
-
path with inline text or a permanent artifact URL, then rerun.
|
|
195
|
-
|
|
196
|
-
Recovery example: one old record remains after an interrupted legacy account
|
|
197
|
-
swap. The user selects that exact file and confirms its session is inactive.
|
|
198
|
-
The recovery command restores the named account and deletes only that record.
|
|
169
|
+
Read `reference/examples.md` for a create, a rejected comment, and a recovery.
|
|
199
170
|
|
|
200
171
|
## Gotchas
|
|
201
172
|
|
|
202
173
|
- Relative body-file paths depend on the caller directory. Use an absolute path
|
|
203
174
|
when another process or worktree starts the command.
|
|
204
|
-
- A record older than 30 minutes can still belong to a live session. Require
|
|
205
|
-
`--confirm-inactive` before recovery.
|
|
206
175
|
- Normal pull request actions never call `gh auth switch`. If that command
|
|
207
176
|
appears outside explicit recovery, stop.
|
|
208
177
|
- `gh auth switch`, `login`, and `logout` change the account for every session
|
|
@@ -217,6 +186,8 @@ The recovery command restores the named account and deletes only that record.
|
|
|
217
186
|
|---|---|
|
|
218
187
|
| `SKILL.md` | Hub for pull request publication, validation, author selection, recovery, and readback |
|
|
219
188
|
| `reference/publication-tasks.md` | Ordered tasks for every publication gate |
|
|
189
|
+
| `reference/legacy-author-recovery.md` | Recovery of one selected legacy author record |
|
|
190
|
+
| `reference/examples.md` | Worked create, rejected comment, and recovery examples |
|
|
220
191
|
| `scripts/pull_request.py` | Command for author selection and create, edit, comment, and review actions |
|
|
221
192
|
| `scripts/github_pr_command_constants/` | Constants for pull request actions, recovery, exit codes, and generic messages |
|
|
222
193
|
| `scripts/test_pull_request.py` | Tests for `pull_request.py` |
|
|
@@ -226,18 +197,3 @@ The recovery command restores the named account and deletes only that record.
|
|
|
226
197
|
| `<managed-root>/scripts/durable_post_lint.py` | Shared action-aware title, body, and path validator |
|
|
227
198
|
| `scripts/gh_artifact_upload.py` | Helper for permanent GitHub binary evidence URLs |
|
|
228
199
|
| `scripts/tests/test_gh_artifact_upload.py` | Tests for binary evidence upload |
|
|
229
|
-
|
|
230
|
-
## Folder map
|
|
231
|
-
|
|
232
|
-
```text
|
|
233
|
-
pull-request/
|
|
234
|
-
├── SKILL.md
|
|
235
|
-
├── reference/
|
|
236
|
-
│ └── publication-tasks.md
|
|
237
|
-
└── scripts/
|
|
238
|
-
├── pull_request.py
|
|
239
|
-
├── github_pr_command_constants/
|
|
240
|
-
├── recover_legacy_author.py
|
|
241
|
-
├── test_pull_request.py
|
|
242
|
-
└── test_recover_legacy_author.py
|
|
243
|
-
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Pull request examples
|
|
2
|
+
|
|
3
|
+
Create example: the writer produces `pr-body.md`. The linter exits `0` for
|
|
4
|
+
`pr-create`. `pull_request.py create` publishes one ready pull request. The
|
|
5
|
+
readback matches the title, body, head SHA, and draft state.
|
|
6
|
+
|
|
7
|
+
Rejected comment example: the comment body names a worktree file. The linter
|
|
8
|
+
returns a non-zero exit. No account lookup or GitHub request runs. Replace the
|
|
9
|
+
path with inline text or a permanent artifact URL, then rerun.
|
|
10
|
+
|
|
11
|
+
Recovery example: one old record remains after an interrupted legacy account
|
|
12
|
+
swap. The user selects that exact file and confirms its session is inactive.
|
|
13
|
+
The recovery command restores the named account and deletes only that record.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Recover a selected legacy author record
|
|
2
|
+
|
|
3
|
+
Run `.agents/skills/pull-request/scripts/recover_legacy_author.py
|
|
4
|
+
<exact-state-file> --confirm-inactive` only when the user selects one legacy
|
|
5
|
+
state file. The command checks the record's age, secure file metadata, and
|
|
6
|
+
contents. The confirmation flag records that the caller verified inactivity.
|
|
7
|
+
Leave every other record untouched. Delete the selected record only after a
|
|
8
|
+
successful restore.
|
|
9
|
+
|
|
10
|
+
A record older than 30 minutes can still belong to a live session. Require
|
|
11
|
+
`--confirm-inactive` before recovery.
|
|
@@ -32,7 +32,7 @@ const PACKAGE_SETTINGS_PATH = join(PACKAGE_ROOT, 'settings.json');
|
|
|
32
32
|
const INSTALL_ENTRY = join(PACKAGE_ROOT, 'bin', 'install.mjs');
|
|
33
33
|
|
|
34
34
|
const EXPECTED_DENY_ENTRIES = [];
|
|
35
|
-
const EXPECTED_ALLOW_ENTRIES = ['WebFetch(domain:docs.github.com)'];
|
|
35
|
+
const EXPECTED_ALLOW_ENTRIES = ['WebFetch(domain:docs.github.com)', 'Read(~/.claude/docs/**)'];
|
|
36
36
|
const PROFILE_ALLOW_ENTRIES = DECLARED_PROFILE_SETTINGS
|
|
37
37
|
.find((eachEntry) => eachEntry.keyPath.join('.') === 'permissions.allow').items;
|
|
38
38
|
const SAMPLE_MANAGED_DENY_ENTRIES = ['Edit($HOME/.claude/managed-test/**)'];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Working style
|
|
2
|
+
|
|
3
|
+
The SessionStart hook `hooks/session/working_style_prompt.py` points every session here. Read this file once, before your first reply.
|
|
4
|
+
|
|
5
|
+
## Records
|
|
6
|
+
|
|
7
|
+
- Document each task in a location that remains easy to find later.
|
|
8
|
+
- Keep a running scratch text ledger as you work.
|
|
9
|
+
|
|
10
|
+
## Presentation
|
|
11
|
+
|
|
12
|
+
- Use ELI5 for beginner framing, large visuals, minimal text, one stable self-contained HTML artifact, update-in-place continuity, and sharing when a user-facing response needs that presentation.
|
|
13
|
+
- Apply `~/.claude/rules/asd-ste100-language.md` for user-facing word choice, sentence style, tone, punctuation, and prose form.
|
|
14
|
+
- Keep responses focused, brief, and concise. Keep disclaimers and caveats short while giving the main answer most of the response.
|
|
15
|
+
- Give a high-level explanation by default and provide depth when the request calls for it.
|
|
16
|
+
- Match written-document length to the task. Cover the substance and keep every section, summary, and phrase useful.
|
|
17
|
+
- Use current, immediately relevant context. Use full terms and specific names for repository work. Keep all text concise, clear, direct, and useful.
|
|
18
|
+
|
|
19
|
+
## Replies
|
|
20
|
+
|
|
21
|
+
- On a typed request, state your next action in one sentence before the first tool call.
|
|
22
|
+
- Finish with the outcome in the first sentence.
|
|
23
|
+
- Send the user only what they must act on or need to know.
|
|
24
|
+
- When a background event, such as a task notification, an agent message, or a scheduled wake, starts a turn and nothing in it needs the user, end the turn with no text.
|
|
25
|
+
- A report that a fix is done carries three lines: the fix acknowledged, what changed, and the proof that it works. Put the rest of the detail in the pull request or a linked file.
|
|
26
|
+
|
|
27
|
+
## Scope
|
|
28
|
+
|
|
29
|
+
- Deliver the requested work at its intended scope. Make routine judgment calls yourself.
|
|
30
|
+
- Ask for direction when different interpretations would produce materially different work.
|
|
31
|
+
- When a request seems mistaken or a better approach exists, state the concern briefly and continue with the requested task.
|
|
32
|
+
- A request to remove something is complete once it is gone. Put nothing in its place, and pass a replacement idea to the requester as a question.
|
|
33
|
+
- Finish the complete task and keep actions within the requested scope.
|
|
34
|
+
|
|
35
|
+
## Questions
|
|
36
|
+
|
|
37
|
+
- When a request has multiple reasonable interpretations, state your understanding and the assumptions that shape the work.
|
|
38
|
+
- Ask one focused clarification question when the ambiguity changes the outcome, scope, audience, format, or risk.
|
|
39
|
+
- Use a clearly stated low-risk assumption when the intended result remains stable.
|
|
40
|
+
- Pause for the user's choice before making a high-impact decision.
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PreToolUse hook that stops an artifact page from publishing when dark mode hides its text.
|
|
3
|
+
|
|
4
|
+
The artifact service wraps every published page in a skeleton whose style pins
|
|
5
|
+
``body`` to a light background and dark text. A page that flips its theme
|
|
6
|
+
tokens on ``html`` alone keeps that light body in dark mode, and its light
|
|
7
|
+
dark-mode text lands on it. A plain local render has no wrapper, so the page
|
|
8
|
+
looks fine there and fails once published.
|
|
9
|
+
|
|
10
|
+
This gate renders the page inside that same wrapper in headless Chromium,
|
|
11
|
+
once under ``prefers-color-scheme: dark`` and once with ``data-theme="dark"``
|
|
12
|
+
on the root element::
|
|
13
|
+
|
|
14
|
+
Artifact {"action": "publish", "file_path": "page.html"} -> rendered
|
|
15
|
+
body stays light, or visible text under 4.5:1 -> denied
|
|
16
|
+
every check holds -> allowed
|
|
17
|
+
Artifact {"action": "read", ...} -> allowed
|
|
18
|
+
Artifact {"asset": true, ...} or {"type_url": ...} -> allowed
|
|
19
|
+
a publish whose file_path is not .html -> allowed
|
|
20
|
+
|
|
21
|
+
A host without Playwright or Chromium is denied with the install command,
|
|
22
|
+
because an unrendered page is the case this gate exists to stop.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import subprocess
|
|
29
|
+
import sys
|
|
30
|
+
import tempfile
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
hooks_root_directory = str(Path(__file__).resolve().parent.parent)
|
|
34
|
+
if hooks_root_directory not in sys.path:
|
|
35
|
+
sys.path.insert(0, hooks_root_directory)
|
|
36
|
+
|
|
37
|
+
from hooks_constants.artifact_dark_mode_gate_constants import (
|
|
38
|
+
ALL_SKIPPED_PUBLISH_INPUT_KEYS,
|
|
39
|
+
ALLOW_EXIT_CODE,
|
|
40
|
+
ARTIFACT_ACTION_INPUT_KEY,
|
|
41
|
+
ARTIFACT_FILE_PATH_INPUT_KEY,
|
|
42
|
+
ARTIFACT_PUBLISH_ACTION,
|
|
43
|
+
ARTIFACT_TOOL_NAME,
|
|
44
|
+
BLOCK_EXIT_CODE,
|
|
45
|
+
DARK_MODE_FAILURE_HEADER,
|
|
46
|
+
GATE_EVENT_NAME,
|
|
47
|
+
HTML_FILE_SUFFIX,
|
|
48
|
+
PUBLISH_SKELETON_PREFIX,
|
|
49
|
+
RENDERER_MISSING_EXIT_CODE,
|
|
50
|
+
RENDERER_MISSING_HEADER,
|
|
51
|
+
RENDERER_SCRIPT_PATH,
|
|
52
|
+
RENDERER_TIMEOUT_SECONDS,
|
|
53
|
+
RETRY_INSTRUCTION,
|
|
54
|
+
)
|
|
55
|
+
from hooks_constants.hook_block_logger import log_hook_block
|
|
56
|
+
from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin
|
|
57
|
+
from hooks_constants.spawn_readiness_hook_constants import TOOL_INPUT_KEY, TOOL_NAME_KEY
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def published_page_path(tool_name: object, tool_input: object) -> Path | None:
|
|
61
|
+
"""Return the local HTML page a call publishes as an artifact page, or None.
|
|
62
|
+
|
|
63
|
+
>>> published_page_path("Artifact", {"file_path": "/tmp/page.html"})
|
|
64
|
+
PosixPath('/tmp/page.html')
|
|
65
|
+
>>> published_page_path("Artifact", {"action": "read", "url": "x"}) is None
|
|
66
|
+
True
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
tool_name: The tool the session called.
|
|
70
|
+
tool_input: The tool input.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
The page path for a page publish, or None for any other call.
|
|
74
|
+
"""
|
|
75
|
+
if tool_name != ARTIFACT_TOOL_NAME or not isinstance(tool_input, dict):
|
|
76
|
+
return None
|
|
77
|
+
if (
|
|
78
|
+
tool_input.get(ARTIFACT_ACTION_INPUT_KEY, ARTIFACT_PUBLISH_ACTION)
|
|
79
|
+
!= ARTIFACT_PUBLISH_ACTION
|
|
80
|
+
):
|
|
81
|
+
return None
|
|
82
|
+
if any(tool_input.get(each_key) for each_key in ALL_SKIPPED_PUBLISH_INPUT_KEYS):
|
|
83
|
+
return None
|
|
84
|
+
file_path = tool_input.get(ARTIFACT_FILE_PATH_INPUT_KEY)
|
|
85
|
+
if not isinstance(file_path, str) or not file_path.lower().endswith(HTML_FILE_SUFFIX):
|
|
86
|
+
return None
|
|
87
|
+
return Path(file_path)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def wrap_in_publish_skeleton(page_source: str) -> str:
|
|
91
|
+
"""Return the page as the artifact service serves it, inside its skeleton.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
page_source: The HTML the session is about to publish.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
The skeleton head and body opening followed by the page source.
|
|
98
|
+
"""
|
|
99
|
+
return PUBLISH_SKELETON_PREFIX + page_source
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _run_renderer(wrapped_page_path: Path) -> subprocess.CompletedProcess[str] | str:
|
|
103
|
+
try:
|
|
104
|
+
return subprocess.run(
|
|
105
|
+
["node", str(RENDERER_SCRIPT_PATH), str(wrapped_page_path)],
|
|
106
|
+
capture_output=True,
|
|
107
|
+
text=True,
|
|
108
|
+
timeout=RENDERER_TIMEOUT_SECONDS,
|
|
109
|
+
check=False,
|
|
110
|
+
)
|
|
111
|
+
except FileNotFoundError:
|
|
112
|
+
return "node is not installed; install Node.js, then run: npm install -g playwright && npx playwright install chromium"
|
|
113
|
+
except subprocess.TimeoutExpired:
|
|
114
|
+
return f"the render took longer than {RENDERER_TIMEOUT_SECONDS} seconds"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def render_findings(page_source: str) -> tuple[dict[str, list[str]] | None, str]:
|
|
118
|
+
"""Render the wrapped page in both dark signals and return what failed.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
page_source: The HTML the session is about to publish.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
The findings by dark signal (empty when the page passes) and an empty
|
|
125
|
+
message, or None and the reason the renderer could not run.
|
|
126
|
+
"""
|
|
127
|
+
with tempfile.TemporaryDirectory() as scratch_directory:
|
|
128
|
+
wrapped_page_path = Path(scratch_directory) / "wrapped_page.html"
|
|
129
|
+
wrapped_page_path.write_text(wrap_in_publish_skeleton(page_source), encoding="utf-8")
|
|
130
|
+
completed_render = _run_renderer(wrapped_page_path)
|
|
131
|
+
if isinstance(completed_render, str):
|
|
132
|
+
return None, completed_render
|
|
133
|
+
if completed_render.returncode == RENDERER_MISSING_EXIT_CODE:
|
|
134
|
+
return None, completed_render.stderr.strip()
|
|
135
|
+
try:
|
|
136
|
+
findings_by_signal = json.loads(completed_render.stdout)
|
|
137
|
+
except json.JSONDecodeError:
|
|
138
|
+
return None, completed_render.stderr.strip() or "the renderer printed no result"
|
|
139
|
+
return findings_by_signal, ""
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def format_block_reason(findings_by_signal: dict[str, list[str]]) -> str:
|
|
143
|
+
"""Return the deny message that lists each failed check under its dark signal.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
findings_by_signal: Findings keyed by the dark signal that produced them.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
The message the session reads when the publish is denied.
|
|
150
|
+
"""
|
|
151
|
+
all_lines = [DARK_MODE_FAILURE_HEADER]
|
|
152
|
+
for each_signal, each_signal_findings in findings_by_signal.items():
|
|
153
|
+
all_lines.append(f"Under {each_signal}:\n")
|
|
154
|
+
all_lines.extend(f"- {each_finding}\n" for each_finding in each_signal_findings)
|
|
155
|
+
return "".join(all_lines) + RETRY_INSTRUCTION
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def main() -> int:
|
|
159
|
+
hook_input = read_hook_input_dictionary_from_stdin()
|
|
160
|
+
if hook_input is None:
|
|
161
|
+
return ALLOW_EXIT_CODE
|
|
162
|
+
page_path = published_page_path(hook_input.get(TOOL_NAME_KEY), hook_input.get(TOOL_INPUT_KEY))
|
|
163
|
+
if page_path is None or not page_path.is_file():
|
|
164
|
+
return ALLOW_EXIT_CODE
|
|
165
|
+
findings_by_signal, renderer_problem = render_findings(page_path.read_text(encoding="utf-8"))
|
|
166
|
+
if findings_by_signal is None:
|
|
167
|
+
block_reason = RENDERER_MISSING_HEADER + renderer_problem
|
|
168
|
+
elif findings_by_signal:
|
|
169
|
+
block_reason = format_block_reason(findings_by_signal)
|
|
170
|
+
else:
|
|
171
|
+
return ALLOW_EXIT_CODE
|
|
172
|
+
log_hook_block(
|
|
173
|
+
Path(__file__).name,
|
|
174
|
+
GATE_EVENT_NAME,
|
|
175
|
+
block_reason,
|
|
176
|
+
tool_name=ARTIFACT_TOOL_NAME,
|
|
177
|
+
offending_input_preview=str(page_path),
|
|
178
|
+
)
|
|
179
|
+
sys.stderr.write(block_reason)
|
|
180
|
+
return BLOCK_EXIT_CODE
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
if __name__ == "__main__":
|
|
184
|
+
sys.exit(main())
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const childProcess = require("child_process");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
|
|
8
|
+
const MINIMUM_CONTRAST_RATIO = 4.5;
|
|
9
|
+
const MAXIMUM_DARK_PAGE_LUMINANCE = 0.2;
|
|
10
|
+
const MAXIMUM_REPORTED_TEXT_FAILURES = 8;
|
|
11
|
+
const TEXT_SNIPPET_LENGTH = 60;
|
|
12
|
+
const PAGE_LOAD_TIMEOUT_MILLISECONDS = 15000;
|
|
13
|
+
const RENDERER_MISSING_EXIT_CODE = 3;
|
|
14
|
+
const ALL_DARK_SIGNALS = [
|
|
15
|
+
{ name: "prefers-color-scheme: dark", colorScheme: "dark", dataTheme: null },
|
|
16
|
+
{ name: 'data-theme="dark"', colorScheme: "light", dataTheme: "dark" },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function loadPlaywright() {
|
|
20
|
+
try {
|
|
21
|
+
return require("playwright");
|
|
22
|
+
} catch (localError) {
|
|
23
|
+
const globalRoot = childProcess.execSync("npm root -g", { encoding: "utf8" }).trim();
|
|
24
|
+
return require(path.join(globalRoot, "playwright"));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function measurePage([maximumDarkPageLuminance, minimumContrastRatio, snippetLength]) {
|
|
29
|
+
const parseColor = (colorText) => {
|
|
30
|
+
const channels = (colorText.match(/[\d.]+/g) || []).map(Number);
|
|
31
|
+
return { red: channels[0] || 0, green: channels[1] || 0, blue: channels[2] || 0, alpha: channels.length > 3 ? channels[3] : 1 };
|
|
32
|
+
};
|
|
33
|
+
const channelLuminance = (channel) => {
|
|
34
|
+
const scaled = channel / 255;
|
|
35
|
+
return scaled <= 0.03928 ? scaled / 12.92 : Math.pow((scaled + 0.055) / 1.055, 2.4);
|
|
36
|
+
};
|
|
37
|
+
const luminance = (color) => 0.2126 * channelLuminance(color.red) + 0.7152 * channelLuminance(color.green) + 0.0722 * channelLuminance(color.blue);
|
|
38
|
+
const contrastRatio = (first, second) => {
|
|
39
|
+
const lighter = Math.max(luminance(first), luminance(second));
|
|
40
|
+
const darker = Math.min(luminance(first), luminance(second));
|
|
41
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
42
|
+
};
|
|
43
|
+
const canvasColor = { red: 255, green: 255, blue: 255, alpha: 1 };
|
|
44
|
+
const effectiveBackground = (element) => {
|
|
45
|
+
for (let current = element; current; current = current.parentElement) {
|
|
46
|
+
const background = parseColor(getComputedStyle(current).backgroundColor);
|
|
47
|
+
if (background.alpha > 0.5) {
|
|
48
|
+
return background;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return canvasColor;
|
|
52
|
+
};
|
|
53
|
+
const isVisible = (element) => {
|
|
54
|
+
const style = getComputedStyle(element);
|
|
55
|
+
const box = element.getBoundingClientRect();
|
|
56
|
+
return style.visibility !== "hidden" && style.display !== "none" && Number(style.opacity) > 0 && box.width > 0 && box.height > 0;
|
|
57
|
+
};
|
|
58
|
+
const allFindings = [];
|
|
59
|
+
const pageBackground = effectiveBackground(document.body);
|
|
60
|
+
if (luminance(pageBackground) > maximumDarkPageLuminance) {
|
|
61
|
+
allFindings.push(`the page background stays light (${getComputedStyle(document.body).backgroundColor} on body); give body an explicit background from the theme tokens`);
|
|
62
|
+
}
|
|
63
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
64
|
+
const seenElements = new Set();
|
|
65
|
+
for (let textNode = walker.nextNode(); textNode; textNode = walker.nextNode()) {
|
|
66
|
+
const element = textNode.parentElement;
|
|
67
|
+
const text = textNode.textContent.trim();
|
|
68
|
+
if (!text || !element || seenElements.has(element) || ["SCRIPT", "STYLE", "TEMPLATE", "NOSCRIPT"].includes(element.tagName)) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
seenElements.add(element);
|
|
72
|
+
if (!isVisible(element)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const foreground = parseColor(getComputedStyle(element).color);
|
|
76
|
+
const background = effectiveBackground(element);
|
|
77
|
+
const ratio = contrastRatio(foreground, background);
|
|
78
|
+
if (ratio < minimumContrastRatio) {
|
|
79
|
+
allFindings.push(`text "${text.slice(0, snippetLength)}" has contrast ${ratio.toFixed(2)}:1 (${getComputedStyle(element).color} on rgb(${background.red}, ${background.green}, ${background.blue}))`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return allFindings;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function checkSignal(browser, pageHtml, darkSignal) {
|
|
86
|
+
const page = await browser.newPage({ colorScheme: darkSignal.colorScheme });
|
|
87
|
+
page.setDefaultTimeout(PAGE_LOAD_TIMEOUT_MILLISECONDS);
|
|
88
|
+
try {
|
|
89
|
+
await page.setContent(pageHtml, { waitUntil: "load", timeout: PAGE_LOAD_TIMEOUT_MILLISECONDS }).catch(() => undefined);
|
|
90
|
+
if (darkSignal.dataTheme) {
|
|
91
|
+
await page.evaluate((themeName) => document.documentElement.setAttribute("data-theme", themeName), darkSignal.dataTheme);
|
|
92
|
+
}
|
|
93
|
+
const allFindings = await page.evaluate(measurePage, [MAXIMUM_DARK_PAGE_LUMINANCE, MINIMUM_CONTRAST_RATIO, TEXT_SNIPPET_LENGTH]);
|
|
94
|
+
return allFindings;
|
|
95
|
+
} finally {
|
|
96
|
+
await page.close();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function main() {
|
|
101
|
+
const pagePath = process.argv[2];
|
|
102
|
+
const pageHtml = fs.readFileSync(pagePath, "utf8");
|
|
103
|
+
let playwright;
|
|
104
|
+
try {
|
|
105
|
+
playwright = loadPlaywright();
|
|
106
|
+
} catch (loadError) {
|
|
107
|
+
process.stderr.write("Playwright is not installed; run: npm install -g playwright && npx playwright install chromium\n");
|
|
108
|
+
return RENDERER_MISSING_EXIT_CODE;
|
|
109
|
+
}
|
|
110
|
+
let browser;
|
|
111
|
+
try {
|
|
112
|
+
browser = await playwright.chromium.launch();
|
|
113
|
+
} catch (launchError) {
|
|
114
|
+
process.stderr.write(`Chromium did not start for Playwright; run: npx playwright install chromium (${launchError.message.split("\n")[0]})\n`);
|
|
115
|
+
return RENDERER_MISSING_EXIT_CODE;
|
|
116
|
+
}
|
|
117
|
+
const report = {};
|
|
118
|
+
try {
|
|
119
|
+
for (const darkSignal of ALL_DARK_SIGNALS) {
|
|
120
|
+
const allFindings = await checkSignal(browser, pageHtml, darkSignal);
|
|
121
|
+
if (allFindings.length > 0) {
|
|
122
|
+
report[darkSignal.name] = allFindings.slice(0, MAXIMUM_REPORTED_TEXT_FAILURES + 1);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} finally {
|
|
126
|
+
await browser.close();
|
|
127
|
+
}
|
|
128
|
+
process.stdout.write(JSON.stringify(report));
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
main().then((exitCode) => process.exit(exitCode));
|