claude-dev-env 1.61.0 → 1.62.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/CLAUDE.md CHANGED
@@ -73,6 +73,14 @@ When asked to sync git ("get X onto origin main", "update main"), fast-forward l
73
73
 
74
74
  For scheduled/cron tasks, default to sub-hour intervals (30-minute); do not propose hourly cadences.
75
75
 
76
+ ## Task Tracking
77
+
78
+ Track every task with the task tool, always — for all sessions and all tasks. Capture each task with `TaskCreate` as it arrives, mark it `in_progress` with `TaskUpdate` when you start, and `completed` when it is done. Run `/task-build` to gather any open tasks and add them to the list in one pass.
79
+
80
+ ## Working in the claude-code-config Repo
81
+
82
+ When changing how skills, rules, or hooks install or sync in this repo (for example adding a skill), read `docs/references/skill-install-system.md` — it maps the install pipeline in `packages/claude-dev-env/bin/install.mjs`.
83
+
76
84
  ## Additional Non-overlapping Rules
77
85
 
78
86
  - **task_scope:** Match every action to what was explicitly requested. When intent is ambiguous, research official docs and present options via AskUserQuestion before making any changes. Proceed with edits only on explicit instruction.
package/bin/install.mjs CHANGED
@@ -149,7 +149,7 @@ const INSTALL_GROUPS = {
149
149
  skills: [
150
150
  'anthropic-plan', 'everything-search',
151
151
  'pr-review-responder',
152
- 'recall', 'remember'
152
+ 'recall', 'remember', 'task-build'
153
153
  ],
154
154
  includeDirectories: ['rules', 'docs', 'commands', 'agents', 'audit-rubrics'],
155
155
  includeAllHooks: true,
@@ -0,0 +1,321 @@
1
+ """Dead config-dataclass field check: cross-module scan for ``*Config`` @dataclass fields.
2
+
3
+ A config ``@dataclass`` (any class whose name ends in ``"Config"``) is defined
4
+ in one module but constructed and consumed in others, so the per-file dead-field
5
+ check in ``code_rules_dead_dataclass_field`` cannot judge its fields — it skips
6
+ any class that is not constructed in the same file. This check resolves the
7
+ enclosing package tree — the scan root — and flags a ``*Config`` dataclass field
8
+ whose name appears as an attribute read (``obj.field``) in no production module
9
+ anywhere under that root.
10
+
11
+ The scan is deliberately conservative to keep false positives near zero:
12
+
13
+ - Only ``@dataclass`` classes whose name ends in ``"Config"`` participate; other
14
+ dataclasses are covered by the per-file check.
15
+ - Test and migration files are exempt as write destinations, so a field added to
16
+ a config dataclass inside a test is never flagged.
17
+ - Production modules under the scan root are scanned for attribute reads; test
18
+ and migration modules are deliberately excluded so a field read only by test
19
+ code is still flagged as dead-in-production.
20
+ - Field reads are collected as ``ast.Attribute.attr`` values (``obj.field``),
21
+ augmented-assignment targets (``cfg.field += 1`` reads ``field`` before
22
+ writing it), string literals (covers ``getattr(obj, "field")``),
23
+ keyword-argument names (covers ``ThemeUpdateConfig(debug_port=1)`` and
24
+ ``replace(cfg, debug_port=1)``), and match-pattern keyword attribute names
25
+ (``case Config(field=found)``). Plain ``ast.Name`` references are excluded — a
26
+ local variable named ``debug_port`` is not a read of ``config.debug_port``.
27
+ - A production module that reflectively reads a whole instance — a bare or
28
+ ``dataclasses``-qualified call to ``asdict``, ``astuple``, ``fields``,
29
+ ``replace``, or ``vars``, or a read of ``obj.__dict__`` — consumes every field
30
+ at once without naming any single field, so the check is suppressed for the
31
+ whole tree (returns ``[]``).
32
+ - A scan root whose total file count exceeds the configured cap cannot prove any
33
+ field dead, so the check returns ``[]`` on a cap hit.
34
+ - A field read only by a module outside the resolved scan root is treated as dead
35
+ — the same conservative scoping the dead-module-constant check accepts.
36
+
37
+ Unlike the per-file dead-dataclass-field check, this cross-module check does NOT
38
+ suppress on a dataclass-dunder whole-instance read — instance comparison
39
+ (``cfg == other``), set or dict membership, formatted-string conversion
40
+ (``f"{cfg}"``), or whole-instance stringification
41
+ (``str(cfg)``/``repr(cfg)``/``format(cfg)``). Those syntactic forms are not bound
42
+ to a config instance, and tree-wide one incidental match anywhere would disable
43
+ the check on any realistic package. The consequence is a documented, rare
44
+ limitation: a ``*Config`` field read ONLY via whole-instance dunder comparison or
45
+ stringification, and never read directly anywhere in production, may be flagged.
46
+ The augmented-assignment read mechanism (``cfg.field += 1`` reads ``field``
47
+ before writing it) is precise and remains a counted read.
48
+ """
49
+
50
+ import ast
51
+ import os
52
+ import sys
53
+ from pathlib import Path
54
+
55
+ _blocking_directory = str(Path(__file__).resolve().parent)
56
+ _hooks_directory = str(Path(__file__).resolve().parent.parent)
57
+ if _blocking_directory not in sys.path:
58
+ sys.path.insert(0, _blocking_directory)
59
+ if _hooks_directory not in sys.path:
60
+ sys.path.insert(0, _hooks_directory)
61
+
62
+ from code_rules_dead_dataclass_field import ( # noqa: E402
63
+ _augmented_assignment_attribute_names,
64
+ _dataclass_field_definitions,
65
+ _is_dataclass,
66
+ )
67
+ from code_rules_dead_module_constant import ( # noqa: E402
68
+ _scan_root_for_constants_module,
69
+ )
70
+ from code_rules_shared import ( # noqa: E402
71
+ is_migration_file,
72
+ is_test_file,
73
+ )
74
+
75
+ from hooks_constants.dead_config_field_constants import ( # noqa: E402
76
+ ALL_REFLECTIVE_FIELD_CONSUMER_NAMES,
77
+ CONFIG_CLASS_NAME_SUFFIX,
78
+ DATACLASSES_MODULE_NAME,
79
+ DEAD_CONFIG_FIELD_GUIDANCE,
80
+ MAX_DEAD_CONFIG_FIELD_ISSUES,
81
+ MAX_SCAN_ROOT_FILE_COUNT,
82
+ PYTHON_SOURCE_SUFFIX,
83
+ WHOLE_INSTANCE_DICT_ATTRIBUTE_NAME,
84
+ )
85
+
86
+
87
+ def _is_config_dataclass(class_node: ast.ClassDef) -> bool:
88
+ """Return whether a class is a @dataclass whose name ends in ``"Config"``.
89
+
90
+ Args:
91
+ class_node: The class definition node to test.
92
+
93
+ Returns:
94
+ True when the class carries a ``@dataclass`` decorator and its name ends
95
+ in ``"Config"``.
96
+ """
97
+ return _is_dataclass(class_node) and class_node.name.endswith(CONFIG_CLASS_NAME_SUFFIX)
98
+
99
+
100
+ def _reads_whole_instance_reflectively(tree: ast.Module) -> bool:
101
+ """Return whether a module consumes a whole instance via a reflective read.
102
+
103
+ Detects a bare call to any reflective whole-instance consumer (``asdict``,
104
+ ``astuple``, ``fields``, ``replace``, ``vars`` imported from ``dataclasses``),
105
+ a ``dataclasses``-qualified call to the same consumers
106
+ (``dataclasses.asdict(cfg)``, ``dataclasses.replace(cfg, ...)``), and a read
107
+ of the ``__dict__`` attribute. The method-call form must be ``dataclasses``-
108
+ qualified — an unrelated ``"text".replace(...)`` or ``frame.fields(...)`` on
109
+ another object does not match. Each matched form reads every field of an
110
+ instance at once without naming any single field, so a module that uses one
111
+ cannot prove a config field unread.
112
+
113
+ Args:
114
+ tree: The parsed module to inspect.
115
+
116
+ Returns:
117
+ True when the module makes a bare or ``dataclasses``-qualified call to a
118
+ reflective whole-instance consumer, or reads ``obj.__dict__``.
119
+ """
120
+ for each_node in ast.walk(tree):
121
+ if isinstance(each_node, ast.Attribute):
122
+ if each_node.attr == WHOLE_INSTANCE_DICT_ATTRIBUTE_NAME:
123
+ return True
124
+ continue
125
+ if not isinstance(each_node, ast.Call):
126
+ continue
127
+ function_node = each_node.func
128
+ if isinstance(function_node, ast.Name) and function_node.id in ALL_REFLECTIVE_FIELD_CONSUMER_NAMES:
129
+ return True
130
+ if (
131
+ isinstance(function_node, ast.Attribute)
132
+ and isinstance(function_node.value, ast.Name)
133
+ and function_node.value.id == DATACLASSES_MODULE_NAME
134
+ and function_node.attr in ALL_REFLECTIVE_FIELD_CONSUMER_NAMES
135
+ ):
136
+ return True
137
+ return False
138
+
139
+
140
+ def _attribute_read_names_in_source(source: str) -> tuple[set[str], bool]:
141
+ """Return attribute names read in a module's source and a suppression flag.
142
+
143
+ Collects attribute names via five mechanisms: ``ast.Attribute.attr`` values
144
+ in Load context, augmented-assignment targets (so ``cfg.debug_port += 1``
145
+ contributes ``"debug_port"`` because ``+=`` reads the attribute before
146
+ writing it), string literals (so ``getattr(obj, "field")`` contributes
147
+ ``"field"``), keyword-argument names (so ``ThemeUpdateConfig(debug_port=1)``
148
+ and ``replace(cfg, debug_port=1)`` each contribute ``"debug_port"``), and
149
+ ``ast.MatchClass.kwd_attrs`` names (so ``case Config(field=x)`` contributes
150
+ ``"field"``). The boolean reports whether the module suppresses the
151
+ dead-field check, which it does only when it reflectively reads a whole
152
+ instance — a bare or ``dataclasses``-qualified ``asdict``/``astuple``/
153
+ ``fields``/``replace``/``vars`` call, or an ``obj.__dict__`` read — because
154
+ that pattern reads every field at once without naming any single field, so
155
+ the caller treats it as "cannot prove any field dead". A ``SyntaxError``
156
+ contributes no names and no suppression.
157
+
158
+ Args:
159
+ source: The full text of a ``.py`` module.
160
+
161
+ Returns:
162
+ A (read_names, suppresses_dead_field_check) pair. The name set is every
163
+ attribute name the module reads via any of the five mechanisms above;
164
+ suppresses_dead_field_check is True only when a reflective whole-instance
165
+ read is present.
166
+ """
167
+ try:
168
+ tree = ast.parse(source)
169
+ except SyntaxError:
170
+ return set(), False
171
+ all_read_names: set[str] = _augmented_assignment_attribute_names(tree)
172
+ for each_node in ast.walk(tree):
173
+ if isinstance(each_node, ast.Attribute) and isinstance(each_node.ctx, ast.Load):
174
+ all_read_names.add(each_node.attr)
175
+ elif isinstance(each_node, ast.Constant) and isinstance(each_node.value, str):
176
+ all_read_names.add(each_node.value)
177
+ elif isinstance(each_node, ast.MatchClass):
178
+ all_read_names.update(each_node.kwd_attrs)
179
+ elif isinstance(each_node, ast.keyword) and each_node.arg is not None:
180
+ all_read_names.add(each_node.arg)
181
+ suppresses_dead_field_check = _reads_whole_instance_reflectively(tree)
182
+ return all_read_names, suppresses_dead_field_check
183
+
184
+
185
+ def _all_production_read_names_under_root(
186
+ scan_root: Path,
187
+ written_path: Path,
188
+ written_content: str,
189
+ ) -> tuple[set[str], bool, bool]:
190
+ """Return read names, a cap-hit flag, and a suppression flag for the tree.
191
+
192
+ Scans every production ``.py`` module under ``scan_root`` (excluding test and
193
+ migration files) for attribute reads. The written module's post-edit content
194
+ replaces its on-disk text so the current edit is included. Scanning stops at
195
+ the configured file cap. A module that reflectively reads a whole instance —
196
+ a bare or ``dataclasses``-qualified ``asdict``/``astuple``/``fields``/
197
+ ``replace``/``vars`` call, or an ``obj.__dict__`` read — sets the suppression
198
+ flag, signalling the caller that no field can be proven dead.
199
+
200
+ Args:
201
+ scan_root: The directory tree to scan.
202
+ written_path: The resolved path of the module being written.
203
+ written_content: The post-edit text of the written module.
204
+
205
+ Returns:
206
+ A (read_names, cap_was_hit, suppresses_dead_field_check) triple. The name
207
+ set is the union of attribute reads across every scanned production
208
+ module; cap_was_hit is True when the scan stopped at the configured file
209
+ cap before finishing the tree; suppresses_dead_field_check is True when
210
+ any scanned module reflectively reads a whole instance.
211
+ """
212
+ all_read_names, suppresses_dead_field_check = _attribute_read_names_in_source(written_content)
213
+ written_path_key = os.path.normcase(str(written_path))
214
+ scanned_file_count = 1
215
+ for each_path in scan_root.rglob("*" + PYTHON_SOURCE_SUFFIX):
216
+ if not each_path.is_file():
217
+ continue
218
+ if os.path.normcase(str(each_path.resolve())) == written_path_key:
219
+ continue
220
+ if is_test_file(str(each_path)):
221
+ continue
222
+ if is_migration_file(str(each_path)):
223
+ continue
224
+ scanned_file_count += 1
225
+ if scanned_file_count > MAX_SCAN_ROOT_FILE_COUNT:
226
+ return all_read_names, True, suppresses_dead_field_check
227
+ try:
228
+ sibling_source = each_path.read_text(encoding="utf-8")
229
+ except (OSError, UnicodeDecodeError):
230
+ continue
231
+ sibling_read_names, sibling_suppresses_dead_field_check = _attribute_read_names_in_source(
232
+ sibling_source
233
+ )
234
+ all_read_names |= sibling_read_names
235
+ suppresses_dead_field_check = (
236
+ suppresses_dead_field_check or sibling_suppresses_dead_field_check
237
+ )
238
+ return all_read_names, False, suppresses_dead_field_check
239
+
240
+
241
+ def check_dead_config_dataclass_fields(
242
+ content: str, file_path: str, full_file_content: str | None = None
243
+ ) -> list[str]:
244
+ """Flag a ``*Config`` @dataclass field read by no production module in the package tree.
245
+
246
+ Runs a cross-module scan restricted to ``@dataclass`` classes whose name ends
247
+ in ``"Config"``. For each such config dataclass in the written file, every
248
+ instance field whose name does not appear as an attribute read (``obj.field``),
249
+ augmented-assignment target (``cfg.field += 1``), string literal,
250
+ keyword-argument name (constructor or ``replace`` keyword), or match-pattern
251
+ keyword attribute in any production module under the enclosing scan root is
252
+ flagged as dead. When any production module under the scan root reflectively
253
+ reads a whole instance — a bare or ``dataclasses``-qualified call to
254
+ ``asdict``, ``astuple``, ``fields``, ``replace``, or ``vars``, or a read of
255
+ ``obj.__dict__`` — the check is suppressed for the whole tree and returns
256
+ ``[]``, since that pattern reads every field at once without naming any single
257
+ field. Test and
258
+ migration files are exempt as write destinations; production modules under the
259
+ scan root are scanned while test and migration modules in the tree are excluded
260
+ so fields read only by test code are still flagged as dead-in-production.
261
+ Whole-file analysis runs against ``full_file_content`` when supplied so an Edit
262
+ fragment is judged against the reconstructed post-edit file. A scan root
263
+ exceeding the file cap returns ``[]`` (cannot prove dead). The scan root is
264
+ resolved the same way as the dead-module-constant check: a ``config/`` module's
265
+ root is its parent directory, a module in a package directory's root is the
266
+ package's parent, and a top-level module's root is its enclosing directory.
267
+
268
+ Args:
269
+ content: The new content under validation (Edit fragment or whole file).
270
+ file_path: The destination path, used for the test/migration exemptions
271
+ and scan-root resolution.
272
+ full_file_content: The reconstructed post-edit whole-file content for an
273
+ Edit, or None for a Write where ``content`` is already the whole file.
274
+
275
+ Returns:
276
+ One violation message per dead config dataclass field, capped at the
277
+ configured maximum. Returns an empty list when the file is exempt, no
278
+ qualifying config dataclass is found, the scan root exceeds the file cap,
279
+ or a SyntaxError prevents parsing.
280
+ """
281
+ if is_test_file(file_path):
282
+ return []
283
+ if is_migration_file(file_path):
284
+ return []
285
+ effective_content = content if full_file_content is None else full_file_content
286
+ try:
287
+ tree = ast.parse(effective_content)
288
+ except SyntaxError:
289
+ return []
290
+ all_config_classes = [
291
+ each_node
292
+ for each_node in ast.walk(tree)
293
+ if isinstance(each_node, ast.ClassDef) and _is_config_dataclass(each_node)
294
+ ]
295
+ if not all_config_classes:
296
+ return []
297
+ scan_root = _scan_root_for_constants_module(file_path)
298
+ written_path = Path(file_path).resolve()
299
+ all_read_names, cap_was_hit, suppresses_dead_field_check = (
300
+ _all_production_read_names_under_root(
301
+ scan_root,
302
+ written_path,
303
+ effective_content,
304
+ )
305
+ )
306
+ if cap_was_hit:
307
+ return []
308
+ if suppresses_dead_field_check:
309
+ return []
310
+ all_issues: list[str] = []
311
+ for each_class in all_config_classes:
312
+ for each_field_name, each_field_line in _dataclass_field_definitions(each_class):
313
+ if each_field_name in all_read_names:
314
+ continue
315
+ all_issues.append(
316
+ f"Line {each_field_line}: config dataclass field {each_field_name!r}"
317
+ f" on {each_class.name} - {DEAD_CONFIG_FIELD_GUIDANCE}"
318
+ )
319
+ if len(all_issues) >= MAX_DEAD_CONFIG_FIELD_ISSUES:
320
+ return all_issues
321
+ return all_issues
@@ -52,6 +52,9 @@ from code_rules_constants_config import ( # noqa: E402
52
52
  check_constants_outside_config_advisory,
53
53
  check_file_global_constants_use_count,
54
54
  )
55
+ from code_rules_dead_config_field import ( # noqa: E402
56
+ check_dead_config_dataclass_fields,
57
+ )
55
58
  from code_rules_dead_dataclass_field import ( # noqa: E402
56
59
  check_dead_dataclass_fields,
57
60
  )
@@ -279,6 +282,9 @@ def validate_content(
279
282
  all_issues.extend(
280
283
  check_dead_dataclass_fields(content, file_path, full_file_content)
281
284
  )
285
+ all_issues.extend(
286
+ check_dead_config_dataclass_fields(content, file_path, full_file_content)
287
+ )
282
288
  all_issues.extend(
283
289
  check_dead_module_constants(content, file_path, full_file_content)
284
290
  )
@@ -96,6 +96,7 @@ COMMAND_AFTER_DIRECTORY_CHANGE_PATTERN = r"[;&|\n][\s]*\S"
96
96
  OPTION_WITH_VALUE_STEP = 2
97
97
  ALL_GATED_TOOL_NAMES = ("Bash", "PowerShell")
98
98
  HASH_PREVIEW_LENGTH = 16
99
+ VERIFICATION_BYPASS_MARKER = "# verify-skip"
99
100
  MINTING_AGENT_TYPE = "code-verifier"
100
101
  VERDICT_DIRECTORY_GUARD_MESSAGE = (
101
102
  "BLOCKED: [VERDICT_DIRECTORY_GUARD] Shell access to the verification "