claude-dev-env 8.62.1 → 8.64.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.
Files changed (26) hide show
  1. package/.agents/skills/context-audit/SKILL.md +48 -0
  2. package/.agents/skills/context-audit/references/depth-modes.md +67 -0
  3. package/.agents/skills/context-audit/scripts/context_audit.py +109 -0
  4. package/.agents/skills/context-audit/scripts/context_audit_constants/__init__.py +1 -0
  5. package/.agents/skills/context-audit/scripts/context_audit_constants/config/__init__.py +1 -0
  6. package/.agents/skills/context-audit/scripts/context_audit_constants/config/constants.py +179 -0
  7. package/.agents/skills/context-audit/scripts/context_inventory.py +362 -0
  8. package/.agents/skills/context-audit/scripts/context_report.py +176 -0
  9. package/.agents/skills/context-audit/scripts/context_sources.py +252 -0
  10. package/.agents/skills/context-audit/scripts/test_context_audit.py +217 -0
  11. package/.agents/skills/search-before-acting/SKILL.md +42 -0
  12. package/.agents/skills/search-before-acting/playbooks/build.md +18 -0
  13. package/.agents/skills/search-before-acting/playbooks/delegate.md +9 -0
  14. package/.agents/skills/search-before-acting/playbooks/plan-or-log.md +9 -0
  15. package/.agents/skills/search-before-acting/references/report-shape.md +14 -0
  16. package/.agents/skills/search-before-acting/references/search-places.md +26 -0
  17. package/bin/ever-shipped-skills.mjs +2 -0
  18. package/bin/ever-shipped-skills.test.mjs +4 -0
  19. package/docs/rule-guides/explore-thoroughly.md +10 -0
  20. package/hooks/blocking/pr_lifecycle_skill_gate.py +5 -2
  21. package/hooks/blocking/pull_request_proof.py +50 -8
  22. package/hooks/blocking/test_pr_lifecycle_skill_gate.py +4 -1
  23. package/hooks/blocking/test_pull_request_proof.py +45 -2
  24. package/hooks/hooks_constants/pull_request_proof_constants.py +14 -0
  25. package/package.json +1 -1
  26. package/rules/explore-thoroughly.md +4 -4
@@ -0,0 +1,252 @@
1
+ """Read checkout files and measure them as context rows.
2
+
3
+ A row records one loaded file: its size, its links out, and its depth mode.
4
+ The helpers here read text, parse frontmatter, and follow links and imports.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import re
11
+ import subprocess
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+
15
+ from context_audit_constants.config import constants
16
+ from context_audit_constants.config.constants import DepthMode, RowKind, Trigger
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class ContextRow:
21
+ """One file an agent loads, with its size and how it loads."""
22
+
23
+ path: str
24
+ kind: RowKind
25
+ loader: str
26
+ trigger: Trigger
27
+ bytes: int
28
+ lines: int
29
+ est_tokens: int
30
+ links_out: int
31
+ depth_mode: DepthMode
32
+ cap: int | None
33
+ note: str
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class RowSource:
38
+ """Where a row's file sits and how the agent reaches it."""
39
+
40
+ path: str
41
+ kind: RowKind
42
+ loader: str
43
+ trigger: Trigger
44
+ note: str
45
+
46
+
47
+ def list_tracked_files(root: Path) -> list[Path]:
48
+ """Return the tracked files under *root*, minus caches and vendored folders.
49
+
50
+ Args:
51
+ root: A git checkout.
52
+
53
+ Returns:
54
+ Absolute paths of tracked files that exist on disk.
55
+ """
56
+ listing = subprocess.run(
57
+ constants.ALL_GIT_LIST_FILES_ARGUMENTS,
58
+ cwd=root,
59
+ capture_output=True,
60
+ check=True,
61
+ ).stdout
62
+ all_paths = [
63
+ root / os.fsdecode(each_entry)
64
+ for each_entry in listing.split(constants.GIT_LIST_FILES_SEPARATOR)
65
+ if each_entry
66
+ ]
67
+ return [
68
+ each_path
69
+ for each_path in all_paths
70
+ if not constants.ALL_SKIPPED_PATH_PARTS.intersection(each_path.parts)
71
+ and each_path.is_file()
72
+ ]
73
+
74
+
75
+ def read_text(path: Path) -> str:
76
+ """Return the file text, or an empty string when it cannot be read.
77
+
78
+ Args:
79
+ path: The file to read.
80
+
81
+ Returns:
82
+ The decoded text.
83
+ """
84
+ try:
85
+ return path.read_text(
86
+ encoding=constants.TEXT_ENCODING, errors=constants.TEXT_DECODE_ERRORS
87
+ )
88
+ except OSError:
89
+ return ""
90
+
91
+
92
+ def parse_frontmatter(text: str) -> dict[str, str]:
93
+ """Return the frontmatter fields of a markdown file as flat strings.
94
+
95
+ ::
96
+
97
+ ---
98
+ name: demo
99
+ paths:
100
+ - src/**
101
+ ---
102
+ -> {"name": "demo", "paths": "- src/**"}
103
+
104
+ Args:
105
+ text: The file text.
106
+
107
+ Returns:
108
+ Field text by field name; empty when the file has no frontmatter.
109
+ """
110
+ fence_length = len(constants.FRONTMATTER_FENCE)
111
+ closing_index = text.find(constants.FRONTMATTER_CLOSING_FENCE, fence_length)
112
+ if not text.startswith(constants.FRONTMATTER_FENCE) or closing_index == -1:
113
+ return {}
114
+ field_by_name: dict[str, str] = {}
115
+ current_name = ""
116
+ for each_line in text[fence_length:closing_index].splitlines():
117
+ current_name = _merge_frontmatter_line(field_by_name, current_name, each_line)
118
+ return field_by_name
119
+
120
+
121
+ def _merge_frontmatter_line(
122
+ field_by_name: dict[str, str], current_name: str, line: str
123
+ ) -> str:
124
+ field_match = re.match(constants.FRONTMATTER_FIELD_PATTERN, line)
125
+ if field_match:
126
+ field_by_name[field_match.group(1)] = field_match.group(2).strip()
127
+ return field_match.group(1)
128
+ if current_name and line.startswith(
129
+ constants.ALL_FRONTMATTER_CONTINUATION_PREFIXES
130
+ ):
131
+ field_by_name[current_name] = (
132
+ f"{field_by_name[current_name]} {line.strip()}".strip()
133
+ )
134
+ return current_name
135
+
136
+
137
+ def find_relative_links(source: Path, text: str, root: Path) -> list[Path]:
138
+ """Return the files inside *root* that markdown links in *text* point to.
139
+
140
+ Args:
141
+ source: The file the text came from.
142
+ text: Markdown text.
143
+ root: The checkout root.
144
+
145
+ Returns:
146
+ Resolved link targets that exist inside the checkout.
147
+ """
148
+ all_targets = [
149
+ each_target
150
+ for each_target in re.findall(constants.LINK_PATTERN, text)
151
+ if not re.match(constants.URL_SCHEME_PATTERN, each_target)
152
+ ]
153
+ return _existing_inside(source, all_targets, root)
154
+
155
+
156
+ def find_import_targets(source: Path, text: str, root: Path) -> list[Path]:
157
+ """Return the files inside *root* that ``@path`` import lines name.
158
+
159
+ Args:
160
+ source: The instruction file the text came from.
161
+ text: The instruction text.
162
+ root: The checkout root.
163
+
164
+ Returns:
165
+ Resolved import targets that exist inside the checkout.
166
+ """
167
+ return _existing_inside(source, re.findall(constants.IMPORT_PATTERN, text), root)
168
+
169
+
170
+ def _existing_inside(source: Path, all_targets: list[str], root: Path) -> list[Path]:
171
+ all_candidates = [
172
+ (source.parent / each_target).resolve() for each_target in all_targets
173
+ ]
174
+ return [
175
+ each_candidate
176
+ for each_candidate in all_candidates
177
+ if each_candidate.is_file() and root in each_candidate.parents
178
+ ]
179
+
180
+
181
+ def classify_depth(
182
+ cap: int | None, byte_count: int, line_count: int, link_count: int
183
+ ) -> DepthMode:
184
+ """Return how much depth a file carries against its cap.
185
+
186
+ ::
187
+
188
+ classify_depth(20, 0, 0, 0) -> empty
189
+ classify_depth(None, 900, 30, 0) -> reference
190
+ classify_depth(20, 4000, 64, 2) -> carries
191
+ classify_depth(20, 600, 12, 3) -> points
192
+
193
+ Args:
194
+ cap: The line cap, or None for on-link depth.
195
+ byte_count: File size in bytes.
196
+ line_count: File length in lines.
197
+ link_count: Relative links out of the file.
198
+
199
+ Returns:
200
+ The depth mode; a file over cap lines, or over cap times 120 bytes, carries.
201
+ """
202
+ if byte_count <= constants.EMPTY_FILE_BYTE_LIMIT:
203
+ return DepthMode.EMPTY
204
+ if cap is None:
205
+ return DepthMode.REFERENCE
206
+ if line_count > cap or byte_count > cap * constants.CHARACTERS_PER_CAPPED_LINE:
207
+ return DepthMode.CARRIES
208
+ return DepthMode.POINTS if link_count else DepthMode.LEAN
209
+
210
+
211
+ def count_lines(text: str) -> int:
212
+ """Return the line count of *text*, counting a final unterminated line.
213
+
214
+ Args:
215
+ text: Any text.
216
+
217
+ Returns:
218
+ The number of lines.
219
+ """
220
+ has_unterminated_tail = bool(text) and not text.endswith(constants.LINE_BREAK)
221
+ return text.count(constants.LINE_BREAK) + int(has_unterminated_tail)
222
+
223
+
224
+ def measure_row(
225
+ source: RowSource, text: str, cap: int | None, link_count: int
226
+ ) -> ContextRow:
227
+ """Return the row for *text* loaded from *source*.
228
+
229
+ Args:
230
+ source: Where the file sits and how it loads.
231
+ text: The text the agent receives.
232
+ cap: The line cap, or None for on-link depth.
233
+ link_count: Relative links out of the text.
234
+
235
+ Returns:
236
+ The measured row.
237
+ """
238
+ byte_count = len(text.encode(constants.TEXT_ENCODING))
239
+ line_count = count_lines(text)
240
+ return ContextRow(
241
+ path=source.path,
242
+ kind=source.kind,
243
+ loader=source.loader,
244
+ trigger=source.trigger,
245
+ bytes=byte_count,
246
+ lines=line_count,
247
+ est_tokens=round(byte_count / constants.BYTES_PER_ESTIMATED_TOKEN),
248
+ links_out=link_count,
249
+ depth_mode=classify_depth(cap, byte_count, line_count, link_count),
250
+ cap=cap,
251
+ note=source.note,
252
+ )
@@ -0,0 +1,217 @@
1
+ """Behavior tests for the context audit, run as a command on a temporary git checkout."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import pytest
11
+
12
+ SCRIPT_PATH = Path(__file__).resolve().parent / "context_audit.py"
13
+
14
+
15
+ def write_file(root: Path, relative_path: str, text: str) -> None:
16
+ target = root / relative_path
17
+ target.parent.mkdir(parents=True, exist_ok=True)
18
+ target.write_text(text, encoding="utf-8")
19
+
20
+
21
+ def numbered_lines(count: int) -> str:
22
+ return "".join(f"line {each_index}\n" for each_index in range(count))
23
+
24
+
25
+ @pytest.fixture
26
+ def checkout(tmp_path: Path) -> Path:
27
+ root = tmp_path / "sample-repo"
28
+ write_file(root, "CLAUDE.md", "@AGENTS.md\n@docs/shared.md\n")
29
+ write_file(root, "AGENTS.md", numbered_lines(30))
30
+ write_file(root, "docs/shared.md", "Shared steps. See [deep](deep.md).\n")
31
+ write_file(root, "docs/deep.md", "Deep notes. See [deeper](deeper.md).\n")
32
+ write_file(root, "docs/deeper.md", "Deeper notes.\n")
33
+ write_file(root, "nested/CLAUDE.md", "")
34
+ write_file(
35
+ root,
36
+ ".claude/rules/api.md",
37
+ "---\npaths:\n - src/api/**\n---\nUse the client.\n",
38
+ )
39
+ write_file(root, ".claude/rules/general.md", "Keep diffs small.\n")
40
+ write_file(
41
+ root,
42
+ ".claude/skills/demo/SKILL.md",
43
+ "---\nname: demo\ndescription: Run the demo. Use when asked to demo.\n---\n"
44
+ + numbered_lines(250),
45
+ )
46
+ write_file(root, ".claude/skills/demo/references/guide.md", "Guide body.\n")
47
+ write_file(
48
+ root,
49
+ ".claude/skills/quiet/SKILL.md",
50
+ "---\nname: quiet\ndescription: Hidden.\ndisable-model-invocation: true\n---\nBody.\n",
51
+ )
52
+ write_file(
53
+ root, "tests/fixture/.claude/skills/ghost/SKILL.md", "---\nname: ghost\n---\n"
54
+ )
55
+ write_file(root, "tool.py", '"""' + numbered_lines(20) + '"""\n')
56
+ subprocess.run(["git", "init", "-q"], cwd=root, check=True)
57
+ subprocess.run(["git", "add", "-A"], cwd=root, check=True)
58
+ return root
59
+
60
+
61
+ def run_audit(*all_arguments: str) -> subprocess.CompletedProcess[str]:
62
+ return subprocess.run(
63
+ [sys.executable, str(SCRIPT_PATH), *all_arguments],
64
+ capture_output=True,
65
+ text=True,
66
+ check=False,
67
+ )
68
+
69
+
70
+ def audit_rows(
71
+ checkout: Path, tmp_path: Path, *all_extra_arguments: str
72
+ ) -> dict[tuple[str, str], dict]:
73
+ rows_path = tmp_path / "rows.jsonl"
74
+ completed = run_audit(str(checkout), "--rows", str(rows_path), *all_extra_arguments)
75
+ assert completed.returncode == 0, completed.stderr
76
+ all_rows = [
77
+ json.loads(each_line) for each_line in rows_path.read_text().splitlines()
78
+ ]
79
+ return {(each_row["path"], each_row["kind"]): each_row for each_row in all_rows}
80
+
81
+
82
+ def test_root_instructions_load_at_session_start_and_carry_when_over_cap(
83
+ checkout: Path, tmp_path: Path
84
+ ) -> None:
85
+ row_by_key = audit_rows(checkout, tmp_path)
86
+ agents_row = row_by_key[("AGENTS.md", "instructions")]
87
+ assert agents_row["trigger"] == "session-start"
88
+ assert agents_row["loader"] == "@import from CLAUDE.md"
89
+ assert agents_row["lines"] == 30
90
+ assert agents_row["depth_mode"] == "carries"
91
+ assert row_by_key[("CLAUDE.md", "instructions")]["depth_mode"] == "lean"
92
+
93
+
94
+ def test_nested_empty_instructions_load_on_folder_enter_as_empty(
95
+ checkout: Path, tmp_path: Path
96
+ ) -> None:
97
+ nested_row = audit_rows(checkout, tmp_path)[("nested/CLAUDE.md", "instructions")]
98
+ assert nested_row["trigger"] == "folder-enter"
99
+ assert nested_row["depth_mode"] == "empty"
100
+
101
+
102
+ def test_import_loads_with_importer_and_its_links_follow_on_demand(
103
+ checkout: Path, tmp_path: Path
104
+ ) -> None:
105
+ row_by_key = audit_rows(checkout, tmp_path)
106
+ assert row_by_key[("docs/shared.md", "import")]["trigger"] == "session-start"
107
+ assert row_by_key[("docs/shared.md", "import")]["depth_mode"] == "points"
108
+ deep_row = row_by_key[("docs/deep.md", "linked-doc")]
109
+ deeper_row = row_by_key[("docs/deeper.md", "linked-doc")]
110
+ assert (deep_row["trigger"], deep_row["depth_mode"], deep_row["note"]) == (
111
+ "on-link",
112
+ "reference",
113
+ "link depth 1",
114
+ )
115
+ assert deeper_row["note"] == "link depth 2"
116
+
117
+
118
+ def test_rule_with_paths_loads_on_path_match_and_plain_rule_at_session_start(
119
+ checkout: Path, tmp_path: Path
120
+ ) -> None:
121
+ row_by_key = audit_rows(checkout, tmp_path)
122
+ assert row_by_key[(".claude/rules/api.md", "rule")]["trigger"] == "path-match"
123
+ assert (
124
+ row_by_key[(".claude/rules/general.md", "rule")]["trigger"] == "session-start"
125
+ )
126
+
127
+
128
+ def test_skill_yields_listing_body_and_reference_rows(
129
+ checkout: Path, tmp_path: Path
130
+ ) -> None:
131
+ row_by_key = audit_rows(checkout, tmp_path)
132
+ description_row = row_by_key[(".claude/skills/demo/SKILL.md", "skill-description")]
133
+ body_row = row_by_key[(".claude/skills/demo/SKILL.md", "skill-body")]
134
+ reference_row = row_by_key[
135
+ (".claude/skills/demo/references/guide.md", "skill-reference")
136
+ ]
137
+ assert (description_row["trigger"], description_row["depth_mode"]) == (
138
+ "session-start",
139
+ "points",
140
+ )
141
+ assert (body_row["trigger"], body_row["depth_mode"]) == ("skill-invoke", "carries")
142
+ assert reference_row["trigger"] == "on-link"
143
+
144
+
145
+ def test_disabled_skill_has_no_listing_and_fixture_skill_is_skipped(
146
+ checkout: Path, tmp_path: Path
147
+ ) -> None:
148
+ row_by_key = audit_rows(checkout, tmp_path)
149
+ assert (".claude/skills/quiet/SKILL.md", "skill-description") not in row_by_key
150
+ assert (".claude/skills/quiet/SKILL.md", "skill-body") in row_by_key
151
+ assert not any(
152
+ each_path.startswith("tests/") and each_kind.startswith("skill")
153
+ for each_path, each_kind in row_by_key
154
+ )
155
+
156
+
157
+ def test_module_docstring_loads_on_file_open_and_carries_over_cap(
158
+ checkout: Path, tmp_path: Path
159
+ ) -> None:
160
+ docstring_row = audit_rows(checkout, tmp_path)[("tool.py", "docstring")]
161
+ assert (
162
+ docstring_row["trigger"],
163
+ docstring_row["depth_mode"],
164
+ docstring_row["cap"],
165
+ ) == ("file-open", "carries", 15)
166
+
167
+
168
+ def test_saved_hook_text_counts_at_session_start(
169
+ checkout: Path, tmp_path: Path
170
+ ) -> None:
171
+ hook_text_file = tmp_path / "hook-banner.txt"
172
+ hook_text_file.write_text(numbered_lines(3), encoding="utf-8")
173
+ row_by_key = audit_rows(
174
+ checkout, tmp_path, "--session-start-text", str(hook_text_file)
175
+ )
176
+ hook_row = row_by_key[("hook-banner.txt", "hook-text")]
177
+ assert (hook_row["trigger"], hook_row["lines"], hook_row["depth_mode"]) == (
178
+ "session-start",
179
+ 3,
180
+ "lean",
181
+ )
182
+
183
+
184
+ def test_report_lists_each_cleanup_section(checkout: Path) -> None:
185
+ completed = run_audit(str(checkout))
186
+ assert completed.returncode == 0, completed.stderr
187
+ carries_section, _, after_carries = completed.stdout.partition(
188
+ "## Empty instruction stubs"
189
+ )
190
+ stubs_section, _, split_section = after_carries.partition("## Long skills to split")
191
+ assert "| AGENTS.md | instructions | session-start | 30 | 20 |" in carries_section
192
+ assert "| tool.py | docstring | file-open | 20 | 15 |" in carries_section
193
+ assert "SKILL.md" not in carries_section
194
+ assert stubs_section.strip() == "- nested/CLAUDE.md (instructions)"
195
+ assert (
196
+ "| .claude/skills/demo/SKILL.md | 254 | 200 |"
197
+ in split_section.split("\n", 3)[3]
198
+ )
199
+
200
+
201
+ def test_command_option_is_gone_and_text_file_must_exist(
202
+ checkout: Path, tmp_path: Path
203
+ ) -> None:
204
+ rejected = run_audit(str(checkout), "--session-start-command", "echo hi")
205
+ assert rejected.returncode == 2
206
+ assert "unrecognized arguments" in rejected.stderr
207
+ missing = run_audit(
208
+ str(checkout), "--session-start-text", str(tmp_path / "absent.txt")
209
+ )
210
+ assert missing.returncode == 2
211
+ assert "file not found" in missing.stderr
212
+
213
+
214
+ def test_folder_outside_git_is_refused(tmp_path: Path) -> None:
215
+ completed = run_audit(str(tmp_path))
216
+ assert completed.returncode == 2
217
+ assert "not a git checkout" in completed.stderr
@@ -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.
@@ -18,6 +18,7 @@ export const EVER_SHIPPED_SKILL_NAMES = new Set([
18
18
  'condensing-instructions',
19
19
  'codex-review',
20
20
  'codex-cleanse',
21
+ 'context-audit',
21
22
  'context7-mcp',
22
23
  'copilot-finding-triage',
23
24
  'copilot-review',
@@ -94,6 +95,7 @@ export const EVER_SHIPPED_SKILL_NAMES = new Set([
94
95
  'reviewer-gates',
95
96
  'reviews',
96
97
  'run-claude-dev-env',
98
+ 'search-before-acting',
97
99
  'second-account-workers',
98
100
  'session-continuity',
99
101
  'session-log',
@@ -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