claude-dev-env 8.62.1 → 8.63.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.
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: context-audit
3
+ description: Inventory every file an agent loads from a repository checkout, with size, trigger, and depth mode, then list what to clean up. Use when the user asks to audit the context in a repository, measure instruction or skill load, or find bloated CLAUDE.md, AGENTS.md, rules, or skills.
4
+ ---
5
+
6
+ # Context audit
7
+
8
+ Use this skill when the user says "audit the context in <repository>" or asks what an agent loads from a checkout.
9
+
10
+ ## Run
11
+
12
+ ```
13
+ python "${CLAUDE_SKILL_DIR}/scripts/context_audit.py" <repository-root> --rows <scratch>/rows.jsonl
14
+ ```
15
+
16
+ Add these options when they apply:
17
+
18
+ | Option | When |
19
+ |---|---|
20
+ | `--rules-dir <path>` | The repository ships a folder that installs as the user rules folder. |
21
+ | `--skills-dir <path>` | The repository ships a folder that installs as the user skills folder. |
22
+ | `--session-start-text <file>` | A SessionStart hook prints text. Save that text to a file first. Repeat for each hook. |
23
+
24
+ Paths for `--rules-dir` and `--skills-dir` are relative to the repository root. The script runs no hook command.
25
+
26
+ ## Read the output
27
+
28
+ The report opens with the startup load in lines and estimated tokens, then a table by trigger. The cleanup list follows in three sections.
29
+
30
+ 1. **Carries depth over cap.** An entry file holds depth that belongs in a reference. Move that depth into a linked file and leave a pointer.
31
+ 2. **Empty instruction stubs.** An instruction file or rule with no content. Delete it unless a tool needs the file to exist.
32
+ 3. **Long skills to split.** A skill body over 200 lines. Keep the steps every run uses in `SKILL.md` and move the rest into `references/`.
33
+
34
+ Content used on every invocation stays in the entry file. Moving it behind a link adds a read to every run.
35
+
36
+ Hand the user the cleanup list with paths, line counts, and caps. The rows file holds every row for follow-up questions.
37
+
38
+ [references/depth-modes.md](references/depth-modes.md) has the triggers, the depth modes, the caps table, and the row fields.
39
+
40
+ ## Layout
41
+
42
+ | File | Purpose |
43
+ |---|---|
44
+ | `scripts/context_audit.py` | Command line entry point. Prints the report and writes the rows file. |
45
+ | `scripts/context_inventory.py` | Walks the checkout and records one row per loaded file. |
46
+ | `scripts/context_sources.py` | Reads files, parses frontmatter, follows links and imports, and measures rows. |
47
+ | `scripts/context_report.py` | Formats the summary, the trigger table, and the cleanup list. |
48
+ | `scripts/context_audit_constants/config/constants.py` | Caps, patterns, labels, and report templates. |
@@ -0,0 +1,67 @@
1
+ # Triggers, depth modes, and caps
2
+
3
+ ## Principle
4
+
5
+ Keep entry points thin and references thick. An entry file loads on a trigger the agent does not choose, so every line in it costs context on every run. A reference loads only when a link sends the agent there.
6
+
7
+ Content used on every invocation stays in the entry file. Moving it behind a link adds a file read to every run and saves nothing.
8
+
9
+ ## Triggers
10
+
11
+ | Trigger | What loads the file |
12
+ |---|---|
13
+ | `session-start` | Root `CLAUDE.md`, `AGENTS.md`, `CLAUDE.local.md` and their `@` imports, rules without `paths`, skill descriptions in a project or installed skills folder, and saved SessionStart hook text. |
14
+ | `folder-enter` | Instruction files in a subfolder, and skill descriptions in a nested `.claude/skills` folder. |
15
+ | `path-match` | Rules whose frontmatter has `paths`. They load when the agent reads a matching file. |
16
+ | `skill-invoke` | A skill body. It loads when the skill runs. |
17
+ | `on-link` | Files under a skill folder other than `SKILL.md`, and markdown files reached through a relative link. Link depth counts hops from the first entry file. |
18
+ | `file-open` | A Python module docstring. The agent reads it when it opens the file. |
19
+
20
+ A skill with `disable-model-invocation: true` has no listing row. Skills under folders named `tests` or `fixtures`, or with `archive` in a folder name, are skipped.
21
+
22
+ ## Depth modes
23
+
24
+ | Mode | Meaning |
25
+ |---|---|
26
+ | `empty` | One byte or less. |
27
+ | `carries` | Over the line cap, or over the cap times 120 bytes. |
28
+ | `points` | Within the cap, with relative links out. |
29
+ | `lean` | Within the cap, with no links out. |
30
+ | `reference` | On-link depth. No cap applies. |
31
+
32
+ ## Caps
33
+
34
+ | Kind | Cap |
35
+ |---|---|
36
+ | Root instructions and their imports, saved hook text | 20 lines |
37
+ | Nested instructions and their imports | 12 lines |
38
+ | Rule | 20 lines |
39
+ | Skill body | 200 lines |
40
+ | Module docstring | 15 lines |
41
+ | Skill description | 500 characters. The listing truncates at 1,536. |
42
+
43
+ The caps live in `scripts/context_audit_constants/config/constants.py`.
44
+
45
+ ## Row fields
46
+
47
+ Each line of the rows file is one JSON object.
48
+
49
+ | Field | Meaning |
50
+ |---|---|
51
+ | `path` | Path relative to the repository root, or the hook text file name. |
52
+ | `kind` | `instructions`, `import`, `rule`, `skill-description`, `skill-body`, `skill-reference`, `hook-text`, `linked-doc`, or `docstring`. |
53
+ | `loader` | What reads the file. |
54
+ | `trigger` | One trigger from the table above. |
55
+ | `bytes`, `lines` | Size of the text the agent receives. |
56
+ | `est_tokens` | Bytes divided by 4. An estimate. |
57
+ | `links_out` | Relative markdown links that resolve inside the repository. |
58
+ | `depth_mode` | One mode from the table above. |
59
+ | `cap` | The cap that applies, or null. |
60
+ | `note` | Extra context, such as link depth. |
61
+
62
+ ## Splitting a long file
63
+
64
+ 1. List the steps every run uses. Keep them in the entry file.
65
+ 2. Move each detail that only some runs need into its own file under `references/`.
66
+ 3. Link each moved file from the entry file with one line that says when to read it.
67
+ 4. Rerun the audit and confirm the entry file is `points` or `lean`.
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env python3
2
+ """Inventory every file an agent loads from one checkout, then print a cleanup list.
3
+
4
+ ::
5
+
6
+ python context_audit.py <repo-root> --rows rows.jsonl
7
+ # Context audit: my-repo
8
+ 412 loaded files. Startup loads 96 lines, about 1830 tokens, from 9 files.
9
+ ## Carries depth over cap
10
+ | AGENTS.md | instructions | session-start | 64 | 20 | 1210 |
11
+
12
+ Each JSON row names a path, kind, loader, trigger, size, and depth mode.
13
+ Hook text is read from saved files; the script runs no hook command.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import json
20
+ import sys
21
+ from dataclasses import asdict
22
+ from pathlib import Path
23
+
24
+ from context_audit_constants.config import constants
25
+ from context_inventory import audit_checkout
26
+ from context_report import format_report
27
+ from context_sources import ContextRow
28
+
29
+
30
+ def write_rows(all_rows: list[ContextRow], into: Path) -> None:
31
+ """Write one JSON object per row to *into*.
32
+
33
+ Args:
34
+ all_rows: Inventory rows.
35
+ into: The JSON Lines file to write.
36
+ """
37
+ with into.open("w", encoding=constants.TEXT_ENCODING) as rows_file:
38
+ for each_row in all_rows:
39
+ rows_file.write(json.dumps(asdict(each_row)) + constants.LINE_BREAK)
40
+
41
+
42
+ def _parse_arguments() -> argparse.Namespace:
43
+ parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
44
+ parser.add_argument("root", type=Path, help="the repository checkout to audit")
45
+ parser.add_argument("--rows", type=Path, help="write every row as JSON Lines here")
46
+ parser.add_argument(
47
+ "--rules-dir",
48
+ type=Path,
49
+ help="tracked folder installed as the user rules folder",
50
+ )
51
+ parser.add_argument(
52
+ "--skills-dir",
53
+ type=Path,
54
+ help="tracked folder installed as the user skills folder",
55
+ )
56
+ parser.add_argument(
57
+ "--session-start-text",
58
+ type=Path,
59
+ action="append",
60
+ default=[],
61
+ help="saved text a SessionStart hook prints; repeat for each hook",
62
+ )
63
+ return parser.parse_args()
64
+
65
+
66
+ def _input_error(parsed: argparse.Namespace) -> str | None:
67
+ if not (parsed.root / constants.GIT_FOLDER_NAME).exists():
68
+ return constants.ERROR_NOT_A_CHECKOUT_TEMPLATE.format(path=parsed.root)
69
+ for each_hook_text_file in parsed.session_start_text:
70
+ if not each_hook_text_file.is_file():
71
+ return constants.ERROR_MISSING_FILE_TEMPLATE.format(
72
+ path=each_hook_text_file
73
+ )
74
+ return None
75
+
76
+
77
+ def _resolve_under(root: Path, maybe_folder: Path | None) -> Path | None:
78
+ return (root / maybe_folder).resolve() if maybe_folder else None
79
+
80
+
81
+ def main() -> int:
82
+ """Run the audit and print the report.
83
+
84
+ Returns:
85
+ The process exit code.
86
+ """
87
+ parsed = _parse_arguments()
88
+ error_message = _input_error(parsed)
89
+ if error_message:
90
+ print(error_message, file=sys.stderr)
91
+ return constants.EXIT_CODE_INPUT_ERROR
92
+ root = parsed.root.resolve()
93
+ all_rows = audit_checkout(
94
+ root,
95
+ _resolve_under(root, parsed.rules_dir),
96
+ _resolve_under(root, parsed.skills_dir),
97
+ tuple(parsed.session_start_text),
98
+ )
99
+ report = format_report(root.name, all_rows)
100
+ if parsed.rows:
101
+ write_rows(all_rows, parsed.rows)
102
+ rows_line = constants.REPORT_ROWS_WRITTEN_TEMPLATE.format(path=parsed.rows)
103
+ report += constants.LINE_BREAK + rows_line + constants.LINE_BREAK
104
+ print(report, end="")
105
+ return constants.EXIT_CODE_AUDITED
106
+
107
+
108
+ if __name__ == "__main__":
109
+ sys.exit(main())
@@ -0,0 +1 @@
1
+ """Named constants for the context-audit skill scripts."""
@@ -0,0 +1 @@
1
+ """Configuration for the context audit."""
@@ -0,0 +1,179 @@
1
+ """Constants for the context audit.
2
+
3
+ The enums name each trigger, depth mode, row kind, and cap.
4
+ The plain constants hold the caps, file names, patterns, and labels.
5
+ The report constants hold the headings and row templates.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from enum import StrEnum
11
+
12
+
13
+ class Trigger(StrEnum):
14
+ """When an agent loads a file into its context."""
15
+
16
+ SESSION_START = "session-start"
17
+ FOLDER_ENTER = "folder-enter"
18
+ PATH_MATCH = "path-match"
19
+ SKILL_INVOKE = "skill-invoke"
20
+ ON_LINK = "on-link"
21
+ FILE_OPEN = "file-open"
22
+
23
+
24
+ class DepthMode(StrEnum):
25
+ """How much depth a file carries against its cap."""
26
+
27
+ EMPTY = "empty"
28
+ CARRIES = "carries"
29
+ POINTS = "points"
30
+ LEAN = "lean"
31
+ REFERENCE = "reference"
32
+
33
+
34
+ class RowKind(StrEnum):
35
+ """What role a loaded file plays."""
36
+
37
+ INSTRUCTIONS = "instructions"
38
+ IMPORT = "import"
39
+ RULE = "rule"
40
+ SKILL_DESCRIPTION = "skill-description"
41
+ SKILL_BODY = "skill-body"
42
+ SKILL_REFERENCE = "skill-reference"
43
+ HOOK_TEXT = "hook-text"
44
+ LINKED_DOC = "linked-doc"
45
+ DOCSTRING = "docstring"
46
+
47
+
48
+ class CapKey(StrEnum):
49
+ """Which line cap applies to a row."""
50
+
51
+ ROOT_INSTRUCTIONS = "root-instructions"
52
+ NESTED_INSTRUCTIONS = "nested-instructions"
53
+ RULE = "rule"
54
+ SKILL_BODY = "skill-body"
55
+ DOCSTRING = "docstring"
56
+
57
+
58
+ ALL_LINE_CAP_BY_KEY = {
59
+ CapKey.ROOT_INSTRUCTIONS: 20,
60
+ CapKey.NESTED_INSTRUCTIONS: 12,
61
+ CapKey.RULE: 20,
62
+ CapKey.SKILL_BODY: 200,
63
+ CapKey.DOCSTRING: 15,
64
+ }
65
+ DESCRIPTION_CHARACTER_CAP = 500
66
+ DESCRIPTION_LISTING_CHARACTER_LIMIT = 1536
67
+ CHARACTERS_PER_CAPPED_LINE = 120
68
+ BYTES_PER_ESTIMATED_TOKEN = 4
69
+ EMPTY_FILE_BYTE_LIMIT = 1
70
+ FIRST_LINK_DEPTH = 1
71
+ DESCRIPTION_LINE_COUNT = 1
72
+
73
+ ALL_SKIPPED_PATH_PARTS = frozenset(
74
+ {
75
+ ".git",
76
+ ".venv",
77
+ "node_modules",
78
+ "__pycache__",
79
+ ".pytest_cache",
80
+ ".mypy_cache",
81
+ }
82
+ )
83
+ ALL_TEXT_SUFFIXES = frozenset({".md", ".mdc", ".txt"})
84
+ PYTHON_SUFFIX = ".py"
85
+ MARKDOWN_SUFFIX = ".md"
86
+ ALL_INSTRUCTION_FILE_NAMES = frozenset({"CLAUDE.md", "AGENTS.md", "CLAUDE.local.md"})
87
+ AGENTS_FILE_NAME = "AGENTS.md"
88
+ CLAUDE_FILE_NAME = "CLAUDE.md"
89
+ AGENTS_IMPORT_LINE = "@AGENTS.md"
90
+ SKILL_FILE_NAME = "SKILL.md"
91
+ GIT_FOLDER_NAME = ".git"
92
+ CLAUDE_FOLDER_NAME = ".claude"
93
+ SKILLS_FOLDER_NAME = "skills"
94
+ PLUGINS_FOLDER_NAME = "plugins"
95
+ ALL_PROJECT_RULES_PATH_PARTS = (CLAUDE_FOLDER_NAME, "rules")
96
+ ALL_SKILL_HOME_FOLDER_NAMES = frozenset({CLAUDE_FOLDER_NAME, ".agents"})
97
+ ARCHIVE_FOLDER_MARKER = "archive"
98
+ ALL_FIXTURE_FOLDER_NAMES = frozenset({"tests", "fixtures"})
99
+ MINIMUM_SKILL_PATH_PARTS = 3
100
+
101
+ ALL_GIT_LIST_FILES_ARGUMENTS = ("git", "ls-files", "-z")
102
+ GIT_LIST_FILES_SEPARATOR = b"\0"
103
+ TEXT_ENCODING = "utf-8"
104
+ TEXT_DECODE_ERRORS = "replace"
105
+ LINE_BREAK = "\n"
106
+ NOTE_SEPARATOR = "; "
107
+
108
+ LINK_PATTERN = r"\[[^\]]*\]\(([^)\s#]+)(?:#[^)]*)?\)"
109
+ IMPORT_PATTERN = r"(?m)^\s*@([^\s]+)"
110
+ URL_SCHEME_PATTERN = r"^[a-z]+:"
111
+ FRONTMATTER_FENCE = "---"
112
+ FRONTMATTER_CLOSING_FENCE = "\n---"
113
+ FRONTMATTER_FIELD_PATTERN = r"^([A-Za-z_-]+):\s*(.*)$"
114
+ ALL_FRONTMATTER_CONTINUATION_PREFIXES = (" ", "\t", "-")
115
+ PATHS_FIELD = "paths"
116
+ DESCRIPTION_FIELD = "description"
117
+ WHEN_TO_USE_FIELD = "when_to_use"
118
+ DISABLE_MODEL_INVOCATION_FIELD = "disable-model-invocation"
119
+ TRUE_TEXT = "true"
120
+ DESCRIPTION_JOIN_TEMPLATE = "{description} {when_to_use}"
121
+
122
+ LOADER_HARNESS = "Claude Code harness"
123
+ LOADER_AGENTS_IMPORTED = "@import from CLAUDE.md"
124
+ LOADER_AGENTS_SHADOWED = (
125
+ "not read by Claude Code (sibling CLAUDE.md, no import); Codex reads it"
126
+ )
127
+ LOADER_AGENTS_NATIVE = "Claude Code native AGENTS.md read; Codex reads it"
128
+ LOADER_IMPORT_TEMPLATE = "@import from {source}"
129
+ LOADER_RULE_PROJECT = "Claude Code harness, .claude/rules"
130
+ LOADER_RULE_INSTALLED = "Claude Code harness, installed rules folder"
131
+ LOADER_SKILL_LISTING_TEMPLATE = "skill listing, {location}"
132
+ LOADER_SKILL_INVOCATION = "skill invocation"
133
+ LOADER_SKILL_REFERENCE_TEMPLATE = "read on demand from {source}"
134
+ LOADER_LINK_TEMPLATE = "markdown link from {source}"
135
+ LOADER_DOCSTRING = "module docstring, read when an agent opens the file"
136
+ LOADER_LINKED_DOCSTRING_TEMPLATE = "module docstring linked from {source}"
137
+ LOADER_HOOK_TEXT = "SessionStart hook text"
138
+ LOCATION_PROJECT_SKILLS = "project skills folder"
139
+ LOCATION_NESTED_SKILLS = "nested .claude/skills folder"
140
+ LOCATION_INSTALLED_SKILLS = "installed skills folder"
141
+ LOCATION_PLUGIN_SKILLS = "plugin skills folder"
142
+ NOTE_CODEX_AGENTS = (
143
+ "Codex: loaded when the working directory is at or below this folder"
144
+ )
145
+ NOTE_IMPORT = "imports load in full with the importing file"
146
+ NOTE_INSTALLED_RULE = "installed rule: loads in every project"
147
+ NOTE_DESCRIPTION_TRUNCATED = "listing truncates at 1,536 characters"
148
+ NOTE_LINK_DEPTH_TEMPLATE = "link depth {depth}"
149
+ NOTE_HOOK_TEXT = "measured from the saved hook text file"
150
+ NOTE_PLUGIN_SKILL = "loads when the plugin is installed"
151
+
152
+ REPORT_TITLE_TEMPLATE = "# Context audit: {name}"
153
+ REPORT_SUMMARY_TEMPLATE = (
154
+ "{row_count} loaded files. Startup loads {lines} lines, about "
155
+ "{tokens} tokens, from {files} files."
156
+ )
157
+ REPORT_TRIGGER_HEADING = "## Load by trigger"
158
+ REPORT_TRIGGER_HEADER = "| trigger | files | lines | est tokens |"
159
+ REPORT_FOUR_COLUMN_RULE = "|---|---|---|---|"
160
+ REPORT_TRIGGER_ROW_TEMPLATE = "| {trigger} | {files} | {lines} | {tokens} |"
161
+ REPORT_CARRIES_HEADING = "## Carries depth over cap"
162
+ REPORT_CARRIES_HEADER = "| path | kind | trigger | lines | cap | est tokens |"
163
+ REPORT_SIX_COLUMN_RULE = "|---|---|---|---|---|---|"
164
+ REPORT_CARRIES_ROW_TEMPLATE = (
165
+ "| {path} | {kind} | {trigger} | {lines} | {cap} | {tokens} |"
166
+ )
167
+ REPORT_EMPTY_HEADING = "## Empty instruction stubs"
168
+ REPORT_EMPTY_ROW_TEMPLATE = "- {path} ({kind})"
169
+ REPORT_SPLIT_HEADING = "## Long skills to split"
170
+ REPORT_SPLIT_HEADER = "| path | lines | cap | est tokens |"
171
+ REPORT_SPLIT_ROW_TEMPLATE = "| {path} | {lines} | {cap} | {tokens} |"
172
+ REPORT_NONE_LINE = "None."
173
+ REPORT_ROWS_WRITTEN_TEMPLATE = "Rows written to {path}."
174
+ ALL_STUB_KINDS = frozenset({RowKind.INSTRUCTIONS, RowKind.IMPORT, RowKind.RULE})
175
+
176
+ ERROR_NOT_A_CHECKOUT_TEMPLATE = "not a git checkout: {path}"
177
+ ERROR_MISSING_FILE_TEMPLATE = "file not found: {path}"
178
+ EXIT_CODE_AUDITED = 0
179
+ EXIT_CODE_INPUT_ERROR = 2