claude-dev-env 8.63.0 → 8.65.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/mod-playbooks/SKILL.md +58 -0
- package/.agents/skills/mod-playbooks/references/cloud-drive.md +47 -0
- package/.agents/skills/mod-playbooks/references/desktop-capture.md +41 -0
- package/.agents/skills/mod-playbooks/references/desktop-window.md +79 -0
- package/.agents/skills/search-before-acting/SKILL.md +42 -0
- package/.agents/skills/search-before-acting/playbooks/build.md +18 -0
- package/.agents/skills/search-before-acting/playbooks/delegate.md +9 -0
- package/.agents/skills/search-before-acting/playbooks/plan-or-log.md +9 -0
- package/.agents/skills/search-before-acting/references/report-shape.md +14 -0
- package/.agents/skills/search-before-acting/references/search-places.md +26 -0
- package/bin/ever-shipped-skills.mjs +2 -0
- package/bin/ever-shipped-skills.test.mjs +4 -0
- package/docs/rule-guides/explore-thoroughly.md +10 -0
- package/hooks/blocking/pr_lifecycle_skill_gate.py +5 -2
- package/hooks/blocking/pull_request_proof.py +50 -8
- package/hooks/blocking/test_pr_lifecycle_skill_gate.py +4 -1
- package/hooks/blocking/test_pull_request_proof.py +45 -2
- package/hooks/hooks_constants/pull_request_proof_constants.py +14 -0
- package/package.json +1 -1
- package/rules/explore-thoroughly.md +4 -4
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mod-playbooks
|
|
3
|
+
description: >-
|
|
4
|
+
Map of playbooks for driving a Claude Code mod (plugin) and seeing its screen: press keys, open menus and drawers, hover a reply to show its action row, click a dropdown, type in a field, fire a toast or a question card, and capture each screen as text and PNG. Use when an agent must test a mod, check a fix by looking at it, or show a mod to the operator on their own desktop.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Mod playbooks
|
|
8
|
+
|
|
9
|
+
## Principle
|
|
10
|
+
|
|
11
|
+
An agent proves a mod works by driving it and reading the screen. It needs no person for that. A
|
|
12
|
+
cloud session runs Claude Code in a pseudo-terminal, plays the steps, and reads each capture. The
|
|
13
|
+
operator's desktop comes last, when a person wants to look.
|
|
14
|
+
|
|
15
|
+
## Pick the route
|
|
16
|
+
|
|
17
|
+
| Goal | Route | Reference |
|
|
18
|
+
| --- | --- | --- |
|
|
19
|
+
| Test a mod, or check a fix, with no person | Drive it in a pseudo-terminal and read the captures | [cloud-drive.md](references/cloud-drive.md) |
|
|
20
|
+
| Let the operator watch the mod on their own screen | Open a desktop window from a Remote Control session | [desktop-window.md](references/desktop-window.md) |
|
|
21
|
+
| Prove what the operator's window shows | Capture that window to a PNG | [desktop-capture.md](references/desktop-capture.md) |
|
|
22
|
+
|
|
23
|
+
When the mod's repository ships a driver and playbooks, use them first. Look for a `drive.mjs` beside
|
|
24
|
+
a `playbooks/` folder and a playbook guide in its docs. Read the guide, run the matching playbook, and
|
|
25
|
+
add a playbook for any new screen you test.
|
|
26
|
+
|
|
27
|
+
## Playbook map
|
|
28
|
+
|
|
29
|
+
Each row is one thing an agent does to a mod, and the input that does it. The bytes are in
|
|
30
|
+
[cloud-drive.md](references/cloud-drive.md).
|
|
31
|
+
|
|
32
|
+
| Do this | Input | Check on screen |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| Send a prompt | Type the text, then Enter | The scripted reply |
|
|
35
|
+
| Open a slash-command menu or pane | Type the command, then Enter | The pane's title |
|
|
36
|
+
| Open a pane from a footer pill | Click the pill's label | The pane's title |
|
|
37
|
+
| Move between buttons in a pane | Tab forward, Shift+Tab back, Enter to press | The focused button |
|
|
38
|
+
| Toggle a control | Click its label | The control's new state |
|
|
39
|
+
| Close a pane | Escape | The prompt again |
|
|
40
|
+
| Show a reply's action row | Hover the reply text; the row draws only under the pointer | The action labels |
|
|
41
|
+
| Pick from a dropdown | Click the action, click the choice, click the confirm button | The confirm line |
|
|
42
|
+
| Type in a text field | Click the field label, type, then Enter | The saved text |
|
|
43
|
+
| Fire a toast | Script a tool turn the mod watches, and allow that tool | The toast's words, top right |
|
|
44
|
+
| Raise a question card | Script a question tool turn, then click an option | The answered line |
|
|
45
|
+
|
|
46
|
+
## Self-resolve loop
|
|
47
|
+
|
|
48
|
+
1. Change the mod.
|
|
49
|
+
2. Run the playbook for the screen you changed.
|
|
50
|
+
3. Read the `.txt` capture for words, and open the `.png` for color and layout.
|
|
51
|
+
4. On a failed step, read `failure.png`, fix the cause, and run again.
|
|
52
|
+
5. Keep the passing captures as the proof, then show the operator if they asked to see it.
|
|
53
|
+
|
|
54
|
+
## Limits
|
|
55
|
+
|
|
56
|
+
- The cloud route proves the mod's screens. It runs a mock model, so it proves nothing about model output.
|
|
57
|
+
- Drive input with the mod repository's driver, in the cloud or on the operator's PC. The desktop window is for looking only.
|
|
58
|
+
- A screen that needs the operator's account, files or network stays on the desktop route.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Drive a mod in a pseudo-terminal
|
|
2
|
+
|
|
3
|
+
A cloud session can run Claude Code with a mod loaded, press keys, click, hover, and capture the screen,
|
|
4
|
+
with no PC and no person. Use the mod repository's own driver when it ships one. Build on the same parts
|
|
5
|
+
when it does not.
|
|
6
|
+
|
|
7
|
+
## The parts
|
|
8
|
+
|
|
9
|
+
| Part | Job |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| `node-pty` | Runs `claude --plugin-dir <mod>` in a pseudo-terminal. |
|
|
12
|
+
| `@xterm/headless` | Mirrors the output into a cell buffer, so the screen reads as text with colors. |
|
|
13
|
+
| A local mock of the Messages API | Scripts each model turn, so the model calls the tool that fires the mod. Point `ANTHROPIC_BASE_URL` at it. |
|
|
14
|
+
| A throwaway home and config folder | Seeds onboarding, API key approval and folder trust, so the first screen is the prompt. |
|
|
15
|
+
| A renderer | Paints the cells to HTML and screenshots it with Playwright Chromium, so the capture is a PNG. |
|
|
16
|
+
|
|
17
|
+
## Input bytes
|
|
18
|
+
|
|
19
|
+
| Action | Bytes written to the terminal |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| Enter | `\r` |
|
|
22
|
+
| Tab, Shift+Tab | `\t`, `\x1b[Z` |
|
|
23
|
+
| Escape | `\x1b` |
|
|
24
|
+
| Arrow up, down, right, left | `\x1b[A`, `\x1b[B`, `\x1b[C`, `\x1b[D` |
|
|
25
|
+
| Clear the prompt line | `\x15` |
|
|
26
|
+
| Left click at column `c`, row `r` (1-based) | `\x1b[<0;c;rM` then `\x1b[<0;c;rm` |
|
|
27
|
+
| Hover at column `c`, row `r` | `\x1b[<35;c;rM` |
|
|
28
|
+
|
|
29
|
+
Find `c` and `r` by searching the cell buffer for the text to point at. Search from the bottom row up,
|
|
30
|
+
so a click lands on the newest copy of a label. Skip zero-width cells when you join a row, or wide
|
|
31
|
+
characters shift every column after them.
|
|
32
|
+
|
|
33
|
+
## The loop
|
|
34
|
+
|
|
35
|
+
1. Write the steps: the scripted turns, then each key, click or hover, the text to wait for, and a capture.
|
|
36
|
+
2. Run them. Wait for each step's text, then for the screen to stop changing, before the next step.
|
|
37
|
+
3. Read the text capture for words, and open the PNG for color and layout.
|
|
38
|
+
4. Fix the mod, run again, and keep the passing captures as the proof.
|
|
39
|
+
|
|
40
|
+
## Gotchas
|
|
41
|
+
|
|
42
|
+
- A tool the scripted turn calls needs `--allowedTools <tool>`, or a permission prompt stops the run.
|
|
43
|
+
- Check the request body for your tool id, so the second request ends the turn with text.
|
|
44
|
+
- A mod can refuse to draw a pane below a minimum width. Run at 180 columns unless the steps need less.
|
|
45
|
+
- A toast draws in the top-right corner, over the header rows.
|
|
46
|
+
- A PNG renderer can paint a wide emoji over the next cell. The text capture holds the characters as drawn.
|
|
47
|
+
- A step that runs a script from the project folder fails in the sandbox, because that folder is empty.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Capture a desktop window
|
|
2
|
+
|
|
3
|
+
Run this on the operator's PC after the mod fires. It asks the window to paint itself into a bitmap
|
|
4
|
+
with `PrintWindow`, so another window on top does not spoil the image. Open the demo with
|
|
5
|
+
`wt.exe -w new` and `--suppressApplicationTitle`, so its window keeps the demo title and the block
|
|
6
|
+
below can find it by that title.
|
|
7
|
+
|
|
8
|
+
```powershell
|
|
9
|
+
Add-Type -AssemblyName System.Drawing
|
|
10
|
+
Add-Type @"
|
|
11
|
+
using System; using System.Runtime.InteropServices;
|
|
12
|
+
public struct RECT { public int Left, Top, Right, Bottom; }
|
|
13
|
+
public static class W {
|
|
14
|
+
[DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
|
|
15
|
+
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);
|
|
16
|
+
[DllImport("user32.dll")] public static extern bool PrintWindow(IntPtr h, IntPtr hdc, uint flags);
|
|
17
|
+
}
|
|
18
|
+
"@
|
|
19
|
+
[W]::SetProcessDPIAware() | Out-Null
|
|
20
|
+
$window = Get-Process WindowsTerminal | Where-Object MainWindowTitle -like '*Mod demo*' | Select-Object -First 1
|
|
21
|
+
$rect = New-Object RECT
|
|
22
|
+
[W]::GetWindowRect($window.MainWindowHandle, [ref]$rect) | Out-Null
|
|
23
|
+
$bitmap = New-Object System.Drawing.Bitmap ($rect.Right - $rect.Left), ($rect.Bottom - $rect.Top)
|
|
24
|
+
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
|
|
25
|
+
$hdc = $graphics.GetHdc()
|
|
26
|
+
[W]::PrintWindow($window.MainWindowHandle, $hdc, 2) | Out-Null
|
|
27
|
+
$graphics.ReleaseHdc($hdc)
|
|
28
|
+
$bitmap.Save((Join-Path $env:TEMP 'mod-demo.png'))
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Report the path and size, and say whether the image shows color and the mod's output. The PNG stays on
|
|
32
|
+
the PC; a cloud session that needs to see it stages the file through the device folder tools, or has
|
|
33
|
+
the PC session post it. A toast fades, so capture while it shows, or capture twice.
|
|
34
|
+
|
|
35
|
+
## What went wrong before
|
|
36
|
+
|
|
37
|
+
| Capture | Result |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| `CopyFromScreen` on the window rectangle, demo opened with `new-tab` | 1752 x 936 in color, but it showed the active tab and missed the toast. |
|
|
40
|
+
| `CopyFromScreen` without `SetProcessDPIAware` | The image held the wrong region, with other windows in it. |
|
|
41
|
+
| `SetProcessDPIAware`, then `PrintWindow` with flag 2 (`PW_RENDERFULLCONTENT`) | 2520 x 936 in full color, as reported by the PC session. |
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Open a mod on the operator's desktop
|
|
2
|
+
|
|
3
|
+
A toast, pane or status line shows only in the Claude Code window that loads the mod. A Remote Control
|
|
4
|
+
session has no window the operator can see. So the session opens a new desktop window that loads the
|
|
5
|
+
mod, and the operator only looks at it.
|
|
6
|
+
|
|
7
|
+
## Gotchas
|
|
8
|
+
|
|
9
|
+
- A mod loaded inside the Remote Control session fires its toast there. The operator sees nothing.
|
|
10
|
+
- `/plugin install` plus hot reload asks the operator to click "Enable for this session". `--plugin-dir` on a new window skips that prompt.
|
|
11
|
+
- A child window inherits `NO_COLOR=1` and `CLAUDE_CODE_CHILD_SESSION=1` from the Remote Control process. The window then runs black and white. Remove both variables in the child before `claude` starts.
|
|
12
|
+
- `Start-Process pwsh` opens the old console host. Launch through `wt.exe` to get Windows Terminal.
|
|
13
|
+
- `wt.exe new-tab` lands in an existing Windows Terminal window. A screen capture then shows whichever tab is active. Use `wt.exe -w new` for a window of its own.
|
|
14
|
+
- `wt.exe` is an app execution alias. Call it by its full path under `%LOCALAPPDATA%\Microsoft\WindowsApps`.
|
|
15
|
+
- When the operator already has the same plugin installed, both copies load. A `--settings` file with `{"enabledPlugins":{"<plugin>@<marketplace>":false}}` turned off the installed copy for that one session.
|
|
16
|
+
- A shell or program can rename the tab, and the window title follows it. Pass `--suppressApplicationTitle` so the capture can find the window by its demo title.
|
|
17
|
+
|
|
18
|
+
## The auto mode safety check
|
|
19
|
+
|
|
20
|
+
The Remote Control session runs in auto mode, and its classifier reads the operator's message that
|
|
21
|
+
started the session. Two launches with the same commands went two ways:
|
|
22
|
+
|
|
23
|
+
| Seed message | Result |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| The operator asked to see the mod run: "can i see it in action? build the mod and show me" | The window opened. |
|
|
26
|
+
| The operator described a design change and asked for no run | The first step, writing the launcher script, was refused: `Permission for this action was denied by the Claude Code auto mode classifier. Reason: [Auto-Mode Bypass].` |
|
|
27
|
+
|
|
28
|
+
The likely rule, inferred from these two cases: start the session on the operator's own message that asks
|
|
29
|
+
to see or run the mod, and quote that ask in the instructions. When no such message exists, ask the
|
|
30
|
+
operator in one line whether to open the window, and start the session on the answer. A refused
|
|
31
|
+
step is final for that session; report the refusal text and stop.
|
|
32
|
+
|
|
33
|
+
## Steps
|
|
34
|
+
|
|
35
|
+
1. Start a Remote Control session, on the operator's message that asks to see the mod, in a folder on
|
|
36
|
+
the operator's PC that is already approved.
|
|
37
|
+
2. Clone the mod's branch to a fresh temporary folder:
|
|
38
|
+
|
|
39
|
+
```powershell
|
|
40
|
+
$dir = Join-Path $env:TEMP ('mod-demo-' + (Get-Date -Format 'HHmmss'))
|
|
41
|
+
git clone -q --depth 1 -b <branch> <repo-url> $dir
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
3. Write a launcher script that clears the inherited variables and starts Claude Code with the mod:
|
|
45
|
+
|
|
46
|
+
```powershell
|
|
47
|
+
$script = Join-Path $env:TEMP 'mod-demo.ps1'
|
|
48
|
+
@"
|
|
49
|
+
Remove-Item env:NO_COLOR, env:CLAUDE_CODE_CHILD_SESSION -ErrorAction SilentlyContinue
|
|
50
|
+
Set-Location `$env:USERPROFILE
|
|
51
|
+
claude --plugin-dir '$dir\plugins\<mod>' '<one harmless prompt that fires the mod>'
|
|
52
|
+
"@ | Set-Content -Encoding utf8 $script
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
4. Open it in its own Windows Terminal window with PowerShell 7:
|
|
56
|
+
|
|
57
|
+
```powershell
|
|
58
|
+
$wt = Join-Path $env:LOCALAPPDATA 'Microsoft\WindowsApps\wt.exe'
|
|
59
|
+
Start-Process $wt -ArgumentList "-w new new-tab --title `"Mod demo`" --suppressApplicationTitle pwsh -NoExit -File `"$script`""
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
5. Capture the window with [desktop-capture.md](desktop-capture.md) and check the PNG yourself.
|
|
63
|
+
6. Tell the operator the window is open and what to look at. Hand over a pasteable command only when
|
|
64
|
+
the launch failed, and say why it failed.
|
|
65
|
+
|
|
66
|
+
## Input on the operator's PC
|
|
67
|
+
|
|
68
|
+
To click, hover or type into a mod on the operator's PC, run the mod repository's driver there from the
|
|
69
|
+
Remote Control session, for example `node tools/visual-test/drive.mjs <playbook> <capture-folder>`.
|
|
70
|
+
The driver owns its pseudo-terminal, so no input goes to a desktop window, and the auto mode check has
|
|
71
|
+
nothing to refuse. One PC run of the drawer playbook passed with no refusal, where a session that sent
|
|
72
|
+
operating-system keystrokes to a demo window was refused. Post the driver's PNG captures.
|
|
73
|
+
Never send operating-system keystrokes or clicks to the demo window.
|
|
74
|
+
|
|
75
|
+
## Verification
|
|
76
|
+
|
|
77
|
+
- The operator sees the window without any click.
|
|
78
|
+
- The window shows color and runs in Windows Terminal with PowerShell 7.
|
|
79
|
+
- The mod loads with no failure line, and its toast or pane appears.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: search-before-acting
|
|
3
|
+
description: >-
|
|
4
|
+
Search for what already does an ask before logging, planning, delegating,
|
|
5
|
+
or building it, then report what exists in four points and wait for the
|
|
6
|
+
user. Use when a user asks for new or changed behavior, a tool, a check,
|
|
7
|
+
a workflow, a report, a tracker entry, or a plan, and before writing a
|
|
8
|
+
brief for a worker.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Search before acting
|
|
12
|
+
|
|
13
|
+
The user should hear about work that already does their ask before anyone duplicates it. Search first, report what you find, and let the user decide what to add.
|
|
14
|
+
|
|
15
|
+
## Steps
|
|
16
|
+
|
|
17
|
+
1. Restate the ask as the outcome the user wants, in their words.
|
|
18
|
+
2. Search every place in [`references/search-places.md`](references/search-places.md) that this session can reach. Note each place you could not reach.
|
|
19
|
+
3. Sort each hit as full, partial, or related. A hit is full when it already produces the outcome from step 1.
|
|
20
|
+
4. When any hit is full or partial, stop. Send the four-point report from [`references/report-shape.md`](references/report-shape.md) and wait for the user. When a hit is full, tell the user they already have it.
|
|
21
|
+
5. When nothing is found, say where you looked in one line, then continue with the playbook below.
|
|
22
|
+
|
|
23
|
+
## Playbooks
|
|
24
|
+
|
|
25
|
+
Open the playbook that matches the next move.
|
|
26
|
+
|
|
27
|
+
| Next move | Playbook |
|
|
28
|
+
|---|---|
|
|
29
|
+
| Build or change code, a hook, a skill, a rule, a workflow, or config | [`playbooks/build.md`](playbooks/build.md) |
|
|
30
|
+
| Log the ask in a tracker, or write a plan for it | [`playbooks/plan-or-log.md`](playbooks/plan-or-log.md) |
|
|
31
|
+
| Hand the ask to a worker, a thread, or another session | [`playbooks/delegate.md`](playbooks/delegate.md) |
|
|
32
|
+
|
|
33
|
+
## Layout
|
|
34
|
+
|
|
35
|
+
| File | What it holds |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `SKILL.md` | The steps and the playbook index |
|
|
38
|
+
| `references/search-places.md` | Where to search, with the query to run in each place |
|
|
39
|
+
| `references/report-shape.md` | The four-point report and the "you already have it" answer |
|
|
40
|
+
| `playbooks/build.md` | The search and report before a build, and the pull request section |
|
|
41
|
+
| `playbooks/plan-or-log.md` | The search and report before a tracker entry or plan |
|
|
42
|
+
| `playbooks/delegate.md` | The search before a brief, and what the brief carries |
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Build
|
|
2
|
+
|
|
3
|
+
Use this playbook before you build or change code, a hook, a skill, a rule, a workflow, or config for an ask.
|
|
4
|
+
|
|
5
|
+
1. Run the skill steps. Search the places in [`../references/search-places.md`](../references/search-places.md), with production config and launcher options first when the ask names a running system.
|
|
6
|
+
2. When a hit is full or partial, send the [report](../references/report-shape.md) and wait for the user's answer.
|
|
7
|
+
3. When the user says go, build only the missing part from point 3 of the report. Extend the existing piece where it lives. Start a new piece only when the existing one cannot hold the addition, and say why in the pull request.
|
|
8
|
+
4. When nothing exists, follow `prefer-existing-tools.md` to check open-source options, then build.
|
|
9
|
+
5. Add an "Existing work" section to the pull request body. List each hit with its link or `file:line` and what the pull request adds on top of it. When nothing was found, say so and name the places searched. The pull request gate denies a body without this section.
|
|
10
|
+
|
|
11
|
+
Example section:
|
|
12
|
+
|
|
13
|
+
```markdown
|
|
14
|
+
## Existing work
|
|
15
|
+
|
|
16
|
+
- `scripts/run_batch.py:40` already runs jobs in parallel with `--workers`. This pull request adds a per-job timeout on top of it.
|
|
17
|
+
- Searched open and merged pull requests for "timeout" and "workers"; none add a timeout.
|
|
18
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Delegate
|
|
2
|
+
|
|
3
|
+
Use this playbook before you hand an ask to a worker, a thread, or another session.
|
|
4
|
+
|
|
5
|
+
1. Run the skill steps yourself before you write the brief. The worker starts from your search.
|
|
6
|
+
2. When a hit is full or partial, send the user the [report](../references/report-shape.md) and wait. Delegate only after the user says go.
|
|
7
|
+
3. Put the search result in the brief. Name each existing piece with its link or `file:line`, what it covers, and the missing part the worker builds.
|
|
8
|
+
4. When nothing exists, put the places searched in the brief, so the worker skips them and searches what you could not reach.
|
|
9
|
+
5. Ask the worker to report any existing piece it finds that the brief missed, and to stop on it before it builds.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Plan or log
|
|
2
|
+
|
|
3
|
+
Use this playbook before you add an ask to a tracker, open an issue or card for it, or write a plan for it.
|
|
4
|
+
|
|
5
|
+
1. Run the skill steps. Search open and closed tracker entries for the ask first, then the rest of [`../references/search-places.md`](../references/search-places.md).
|
|
6
|
+
2. When a tracker entry already names the ask, update that entry and link it. Skip the new entry.
|
|
7
|
+
3. When code, config, or a merged pull request already does the ask in full or in part, send the [report](../references/report-shape.md) and wait for the user before you log or plan.
|
|
8
|
+
4. When the user says go, log or plan only the missing part. Link each existing piece in the entry or plan, so the reader starts from what is live.
|
|
9
|
+
5. When nothing exists, write the entry or plan with one line that names the places searched.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Report shape
|
|
2
|
+
|
|
3
|
+
Send this report when the search finds something that does the ask in full or in part. Send it before any log entry, plan, brief, or edit, and wait for the user.
|
|
4
|
+
|
|
5
|
+
1. **What exists and where.** Each hit with a link or `file:line`, and whether it is live.
|
|
6
|
+
2. **What it covers of the ask.** The parts of the outcome it already produces.
|
|
7
|
+
3. **What is missing.** The parts of the outcome nothing produces yet.
|
|
8
|
+
4. **What the addition would add.** What the new work gives on top of what exists, and the smallest change that closes the gap.
|
|
9
|
+
|
|
10
|
+
When nothing is missing, the report is one line: the user already has it, with the link and how to use it.
|
|
11
|
+
|
|
12
|
+
When a place in [`search-places.md`](search-places.md) was out of reach, add one line naming it and the check that would reach it.
|
|
13
|
+
|
|
14
|
+
Keep the report as short as the user's channel asks for. When the channel limits length, put the four points in a linked file and send a short message that says what exists, what is missing, and what the user decides.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Search places
|
|
2
|
+
|
|
3
|
+
Search each place this session can reach. Start from the outcome words in the ask and from the names of the systems it touches. Try two or three phrasings, since an existing feature often uses different words than the ask, such as "workers" for "parallel" or "queue" for "batch".
|
|
4
|
+
|
|
5
|
+
| Place | What to run |
|
|
6
|
+
|---|---|
|
|
7
|
+
| Repository code | `rg -i "<outcome words>"` across each repository the work touches, and a search for the system's entry points and their flags, such as `--help` output and argument parsers |
|
|
8
|
+
| Open pull requests | `gh pr list --state open --search "<words>"`, or the GitHub search tool with `is:pr is:open` |
|
|
9
|
+
| Merged pull requests | `gh pr list --state merged --search "<words>"`, and `git log --oneline -i --grep "<words>"` on the default branch |
|
|
10
|
+
| Hooks | the hook registry, such as `hooks.json` or `settings.json`, and the hook directories, for a gate or reminder that already does the step |
|
|
11
|
+
| Rules and instructions | `rules/`, `AGENTS.md`, `CLAUDE.md`, project instructions, and memory notes |
|
|
12
|
+
| Skills and commands | the installed skills list, `skills/`, `.agents/skills/`, and `commands/` |
|
|
13
|
+
| Workflows and schedules | `.github/workflows/`, scheduled routines, cron entries, and task schedulers |
|
|
14
|
+
| Trackers | open and closed issues, project boards, and task cards that name the ask |
|
|
15
|
+
| Production config | config files, environment settings, feature flags, and launcher options that the running system reads, including values that turn on a mode the ask describes |
|
|
16
|
+
| Run logs and dashboards | recent run output that shows the behavior already happening |
|
|
17
|
+
|
|
18
|
+
## What to record for each hit
|
|
19
|
+
|
|
20
|
+
- The place and a link or `file:line`.
|
|
21
|
+
- One sentence on what it does now.
|
|
22
|
+
- Whether it is live: merged and running, merged and unused, or still open.
|
|
23
|
+
|
|
24
|
+
## When a place is out of reach
|
|
25
|
+
|
|
26
|
+
Name the place and the check that would reach it, such as "the production config on the build machine, read through a remote session". The report states the gap. The user may know the answer at once.
|
|
@@ -95,9 +95,11 @@ export const EVER_SHIPPED_SKILL_NAMES = new Set([
|
|
|
95
95
|
'reviewer-gates',
|
|
96
96
|
'reviews',
|
|
97
97
|
'run-claude-dev-env',
|
|
98
|
+
'search-before-acting',
|
|
98
99
|
'second-account-workers',
|
|
99
100
|
'session-continuity',
|
|
100
101
|
'session-log',
|
|
102
|
+
'mod-playbooks',
|
|
101
103
|
'session-tidy',
|
|
102
104
|
'pr-shared-extraction',
|
|
103
105
|
'show',
|
|
@@ -45,6 +45,10 @@ test('EVER_SHIPPED_SKILL_NAMES includes the correction skill', () => {
|
|
|
45
45
|
assert.equal(EVER_SHIPPED_SKILL_NAMES.has('correction'), true);
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
test('EVER_SHIPPED_SKILL_NAMES includes the search before acting skill', () => {
|
|
49
|
+
assert.equal(EVER_SHIPPED_SKILL_NAMES.has('search-before-acting'), true);
|
|
50
|
+
});
|
|
51
|
+
|
|
48
52
|
test('EVER_SHIPPED_SKILL_NAMES includes the windows scheduled task skill', () => {
|
|
49
53
|
assert.equal(EVER_SHIPPED_SKILL_NAMES.has('windows-scheduled-task'), true);
|
|
50
54
|
});
|
|
@@ -6,6 +6,16 @@ Source: [Anthropic - Overthinking and Excessive Thoroughness](https://platform.c
|
|
|
6
6
|
|
|
7
7
|
Note: This deliberately chooses exploration depth over the "commit and execute quickly" pattern from the same source. Thorough upfront exploration is preferred for the intended workflow.
|
|
8
8
|
|
|
9
|
+
## Search for existing work first
|
|
10
|
+
|
|
11
|
+
Before you log, plan, delegate, or build an ask, search for what already does it in full or in part. An ask is any request for new or changed behavior, a tool, a check, a workflow, a report, a tracker entry, or a plan. The `search-before-acting` skill holds the search places, the four-point report, and a playbook for each next move.
|
|
12
|
+
|
|
13
|
+
When the search finds something, stop before you act and report what exists and where, what it covers of the ask, what is missing, and what the addition would add. When it covers the whole ask, tell the user they already have it. When the search finds nothing, say where you looked and continue.
|
|
14
|
+
|
|
15
|
+
[`prefer-existing-tools.md`](../../rules/prefer-existing-tools.md) and [`build-needs-a-user.md`](../../rules/build-needs-a-user.md) load only when a session touches a script, hook, or manifest path. This rule loads in every session, so the search comes before the plan is set.
|
|
16
|
+
|
|
17
|
+
The pull request gate reads the body of each new pull request for an "Existing work" heading. Under it, list what the search found with a link or `file:line` and what the pull request adds on top of it, or state that nothing was found and name the places searched.
|
|
18
|
+
|
|
9
19
|
## Before committing to an approach
|
|
10
20
|
|
|
11
21
|
- Read the relevant files. Understand what exists before proposing what to change.
|
|
@@ -53,7 +53,7 @@ from hooks_constants.hook_specific_output_keys import (
|
|
|
53
53
|
from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin
|
|
54
54
|
from hooks_constants.setup_project_paths_constants import DECODE_ERRORS_POLICY, UTF8_ENCODING
|
|
55
55
|
from blocking.followup_pr_dedupe import duplicate_followup_reason
|
|
56
|
-
from blocking.pull_request_proof import missing_proof_reason
|
|
56
|
+
from blocking.pull_request_proof import missing_existing_work_reason, missing_proof_reason
|
|
57
57
|
from transcript_skill_scan import is_skill_loaded_after_last_compaction
|
|
58
58
|
|
|
59
59
|
|
|
@@ -199,7 +199,7 @@ def _subagent_transcript_path(all_payload_fields: dict[str, object]) -> str | No
|
|
|
199
199
|
|
|
200
200
|
|
|
201
201
|
def decision_for(all_payload_fields: dict[str, object]) -> dict[str, object] | None:
|
|
202
|
-
"""Return a deny for an unloaded skill, a new pull request with no proof, or a second follow-up.
|
|
202
|
+
"""Return a deny for an unloaded skill, a new pull request with no proof or existing-work section, or a second follow-up.
|
|
203
203
|
|
|
204
204
|
Args:
|
|
205
205
|
all_payload_fields: The parsed PreToolUse input.
|
|
@@ -211,6 +211,9 @@ def decision_for(all_payload_fields: dict[str, object]) -> dict[str, object] | N
|
|
|
211
211
|
proof_reason = missing_proof_reason(all_payload_fields)
|
|
212
212
|
if proof_reason is not None:
|
|
213
213
|
return _deny(proof_reason)
|
|
214
|
+
search_reason = missing_existing_work_reason(all_payload_fields)
|
|
215
|
+
if search_reason is not None:
|
|
216
|
+
return _deny(search_reason)
|
|
214
217
|
duplicate_reason = duplicate_followup_reason(all_payload_fields)
|
|
215
218
|
return None if duplicate_reason is None else _deny(duplicate_reason)
|
|
216
219
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Deny a new pull request whose body
|
|
1
|
+
"""Deny a new pull request whose body lacks a 'Proof in practice' or an 'Existing work' section."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -18,8 +18,10 @@ from hooks_constants.pull_request_proof_constants import (
|
|
|
18
18
|
ALL_PYTHON_PROGRAM_NAMES,
|
|
19
19
|
COMMAND_MARKER,
|
|
20
20
|
CREATE_PULL_REQUEST_TOOL_SUFFIX,
|
|
21
|
+
EXISTING_WORK_HEADING_PATTERN,
|
|
21
22
|
LINE_SEPARATOR,
|
|
22
23
|
MISSING_BODY_REASON,
|
|
24
|
+
MISSING_EXISTING_WORK_REASON,
|
|
23
25
|
MISSING_PROOF_REASON,
|
|
24
26
|
NEXT_HEADING_PATTERN,
|
|
25
27
|
PROOF_HEADING_PATTERN,
|
|
@@ -48,6 +50,16 @@ def _lines_until_heading(all_lines: list[str], heading_level: int) -> list[str]:
|
|
|
48
50
|
return all_lines
|
|
49
51
|
|
|
50
52
|
|
|
53
|
+
def _section_text(body: str, heading_pattern: str) -> str | None:
|
|
54
|
+
all_lines = body.splitlines()
|
|
55
|
+
for each_index, each_line in enumerate(all_lines):
|
|
56
|
+
heading_level = _heading_level(each_line, heading_pattern)
|
|
57
|
+
if heading_level is not None:
|
|
58
|
+
all_section_lines = _lines_until_heading(all_lines[each_index + 1 :], heading_level)
|
|
59
|
+
return LINE_SEPARATOR.join(all_section_lines).strip()
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
51
63
|
def proof_section(body: str) -> str | None:
|
|
52
64
|
"""Return the text under the body's 'Proof in practice' heading, or None.
|
|
53
65
|
|
|
@@ -58,13 +70,20 @@ def proof_section(body: str) -> str | None:
|
|
|
58
70
|
|
|
59
71
|
The section runs to the next heading of the same or a higher level.
|
|
60
72
|
"""
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
return _section_text(body, PROOF_HEADING_PATTERN)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def section_under_existing_work(body: str) -> str | None:
|
|
77
|
+
"""Return the text under the body's 'Existing work' heading, or None.
|
|
78
|
+
|
|
79
|
+
::
|
|
80
|
+
|
|
81
|
+
"## Existing work\\n`a.py:4` runs it\\n## Notes" -> "`a.py:4` runs it"
|
|
82
|
+
"## Summary\\nno existing work heading" -> None
|
|
83
|
+
|
|
84
|
+
The section runs to the next heading of the same or a higher level.
|
|
85
|
+
"""
|
|
86
|
+
return _section_text(body, EXISTING_WORK_HEADING_PATTERN)
|
|
68
87
|
|
|
69
88
|
|
|
70
89
|
def _script_arguments(program: str, all_arguments: list[str]) -> list[str] | None:
|
|
@@ -139,3 +158,26 @@ def missing_proof_reason(all_payload_fields: Mapping[str, object]) -> str | None
|
|
|
139
158
|
if not section or COMMAND_MARKER not in section:
|
|
140
159
|
return MISSING_PROOF_REASON
|
|
141
160
|
return None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def missing_existing_work_reason(all_payload_fields: Mapping[str, object]) -> str | None:
|
|
164
|
+
"""Return a deny reason when a new pull request's body has no existing-work section, else None.
|
|
165
|
+
|
|
166
|
+
::
|
|
167
|
+
|
|
168
|
+
mcp__github__create_pull_request, body has the section -> None
|
|
169
|
+
mcp__github__create_pull_request, body without it -> MISSING_EXISTING_WORK_REASON
|
|
170
|
+
gh pr create --fill -> None
|
|
171
|
+
gh pr edit, git push, any other tool -> None
|
|
172
|
+
|
|
173
|
+
The section is an 'Existing work' heading with text under it. A create
|
|
174
|
+
command whose body this gate cannot read passes here, because
|
|
175
|
+
missing_proof_reason denies it first.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
all_payload_fields: The parsed PreToolUse input.
|
|
179
|
+
"""
|
|
180
|
+
for each_body in _create_bodies(all_payload_fields):
|
|
181
|
+
if each_body is not None and not section_under_existing_work(each_body):
|
|
182
|
+
return MISSING_EXISTING_WORK_REASON
|
|
183
|
+
return None
|
|
@@ -15,7 +15,10 @@ from blocking import pr_lifecycle_skill_gate as gate
|
|
|
15
15
|
from hooks_constants.pr_lifecycle_skill_gate_constants import DENY_REASON
|
|
16
16
|
from hooks_constants.pull_request_proof_constants import MISSING_PROOF_REASON
|
|
17
17
|
|
|
18
|
-
PROVEN_FOLLOWUP_BODY =
|
|
18
|
+
PROVEN_FOLLOWUP_BODY = (
|
|
19
|
+
"Follow-up to #1731\n\n## Existing work\nNothing found in open or merged pull requests.\n\n"
|
|
20
|
+
"## Proof in practice\nRan `python probe.py`.\n"
|
|
21
|
+
)
|
|
19
22
|
|
|
20
23
|
|
|
21
24
|
def _transcript(tmp_path: Path, skill_name: str | None = None, compact: bool = False) -> Path:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Behavior tests for the proof-in-practice
|
|
1
|
+
"""Behavior tests for the proof-in-practice and existing-work checks on a new pull request."""
|
|
2
2
|
|
|
3
3
|
import sys
|
|
4
4
|
from pathlib import Path
|
|
@@ -8,10 +8,19 @@ if str(HOOKS_DIRECTORY) not in sys.path:
|
|
|
8
8
|
sys.path.insert(0, str(HOOKS_DIRECTORY))
|
|
9
9
|
|
|
10
10
|
from blocking import pull_request_proof as gate
|
|
11
|
-
from hooks_constants.pull_request_proof_constants import
|
|
11
|
+
from hooks_constants.pull_request_proof_constants import (
|
|
12
|
+
MISSING_BODY_REASON,
|
|
13
|
+
MISSING_EXISTING_WORK_REASON,
|
|
14
|
+
MISSING_PROOF_REASON,
|
|
15
|
+
)
|
|
12
16
|
|
|
13
17
|
PROVEN_BODY = "## Summary\nAdds a gate.\n\n## Proof in practice\nRan `python run.py`.\n> denied\n"
|
|
14
18
|
UNPROVEN_BODY = "## Summary\nAdds a gate.\n\n## Verification\nUnit tests pass.\n"
|
|
19
|
+
SEARCHED_BODY = (
|
|
20
|
+
"## Summary\nAdds a gate.\n\n## Existing work\n`gate.py:12` runs the check; this adds a flag.\n\n"
|
|
21
|
+
"## Proof in practice\nRan `python run.py`.\n"
|
|
22
|
+
)
|
|
23
|
+
EMPTY_EXISTING_WORK_BODY = "## Existing work\n\n## Proof in practice\nRan `python run.py`.\n"
|
|
15
24
|
|
|
16
25
|
|
|
17
26
|
def _shell(command: str, working_directory: Path | None = None) -> dict[str, object]:
|
|
@@ -90,3 +99,37 @@ def test_other_pull_request_commands_should_pass() -> None:
|
|
|
90
99
|
def test_other_tools_should_pass() -> None:
|
|
91
100
|
payload = {"tool_name": "mcp__github__update_pull_request", "tool_input": {"body": "x"}}
|
|
92
101
|
assert gate.missing_proof_reason(payload) is None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_existing_work_section_should_stop_at_a_heading_of_the_same_level() -> None:
|
|
105
|
+
assert gate.section_under_existing_work(SEARCHED_BODY) == "`gate.py:12` runs the check; this adds a flag."
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_mcp_create_without_existing_work_should_be_denied() -> None:
|
|
109
|
+
assert gate.missing_existing_work_reason(_mcp_create(PROVEN_BODY)) == MISSING_EXISTING_WORK_REASON
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_mcp_create_with_an_empty_existing_work_section_should_be_denied() -> None:
|
|
113
|
+
payload = _mcp_create(EMPTY_EXISTING_WORK_BODY)
|
|
114
|
+
assert gate.missing_existing_work_reason(payload) == MISSING_EXISTING_WORK_REASON
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_mcp_create_with_existing_work_should_pass() -> None:
|
|
118
|
+
assert gate.missing_existing_work_reason(_mcp_create(SEARCHED_BODY)) is None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_gh_create_body_file_without_existing_work_should_be_denied(tmp_path: Path) -> None:
|
|
122
|
+
(tmp_path / "body.md").write_text(PROVEN_BODY, encoding="utf-8")
|
|
123
|
+
payload = _shell("gh pr create --draft --body-file body.md", tmp_path)
|
|
124
|
+
assert gate.missing_existing_work_reason(payload) == MISSING_EXISTING_WORK_REASON
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_unreadable_body_should_leave_the_deny_to_the_proof_check() -> None:
|
|
128
|
+
payload = _shell("gh pr create --fill")
|
|
129
|
+
assert gate.missing_existing_work_reason(payload) is None
|
|
130
|
+
assert gate.missing_proof_reason(payload) == MISSING_BODY_REASON
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_other_pull_request_commands_should_pass_the_existing_work_check() -> None:
|
|
134
|
+
for each_command in ("gh pr edit 3 --body x", "git push -u origin b", "gh pr view"):
|
|
135
|
+
assert gate.missing_existing_work_reason(_shell(each_command)) is None
|
|
@@ -25,3 +25,17 @@ MISSING_PROOF_REASON = (
|
|
|
25
25
|
" prove the change, leave the pull request unopened and tell the user what you could"
|
|
26
26
|
" not run. Read " + PROOF_GUIDE_PATH + " for the full rule."
|
|
27
27
|
)
|
|
28
|
+
EXISTING_WORK_HEADING_PATTERN = r"^(#{1,6})[ \t]+existing work[ \t]*#*[ \t]*$"
|
|
29
|
+
EXISTING_WORK_GUIDE_PATH = "~/.claude/docs/rule-guides/explore-thoroughly.md"
|
|
30
|
+
MISSING_EXISTING_WORK_REASON = (
|
|
31
|
+
"This pull request body has no 'Existing work' section. Before you open a pull"
|
|
32
|
+
" request, search for what already does the change in full or in part: repository"
|
|
33
|
+
" code, open and merged pull requests, hooks, rules, skills, workflows, trackers and"
|
|
34
|
+
" production config. Add an 'Existing work' heading to the body. Under it, list each"
|
|
35
|
+
" piece you found with its link or file:line and what this pull request adds on top"
|
|
36
|
+
" of it, or state that nothing was found and name the places you searched. When the"
|
|
37
|
+
" search finds a piece that already does the change, stop and report it to the user"
|
|
38
|
+
" before you open the pull request. Run the search-before-acting skill and read "
|
|
39
|
+
+ EXISTING_WORK_GUIDE_PATH
|
|
40
|
+
+ " for the full rule."
|
|
41
|
+
)
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Explore thoroughly
|
|
2
2
|
|
|
3
|
-
**When:**
|
|
3
|
+
**When:** Before you log, plan, delegate, or build an ask, or choose an approach.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Search for what already does the ask, in full or in part. When something exists, stop and report it to the user in four points; run the `search-before-acting` skill. Then read files, patterns, and constraints, scaled to risk. Once the files, constraints, and success condition are known, act without repeating settled research.
|
|
6
6
|
|
|
7
|
-
**Enforcement:**
|
|
7
|
+
**Enforcement:** the pull request gate denies a body with no "Existing work" section.
|
|
8
8
|
|
|
9
|
-
**Full text:** [`docs/rule-guides/explore-thoroughly.md`](../docs/rule-guides/explore-thoroughly.md). Read it
|
|
9
|
+
**Full text:** [`docs/rule-guides/explore-thoroughly.md`](../docs/rule-guides/explore-thoroughly.md). Read it for search scope and depth.
|