claude-dev-env 8.0.0 → 8.1.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.
@@ -389,10 +389,6 @@
389
389
  "id": "J3",
390
390
  "axis_name": "Constants location"
391
391
  },
392
- {
393
- "id": "J4",
394
- "axis_name": "File-global use-count"
395
- },
396
392
  {
397
393
  "id": "J5",
398
394
  "axis_name": "Abbreviations"
@@ -26,7 +26,7 @@
26
26
  | E6 | Removed-but-not-deleted symbol references | Symbols renamed/removed elsewhere with stale import or call sites left behind. |
27
27
  | E7 | Test fixtures / helpers defined but never used | Pytest fixtures, test data builders, mock factories with no callers. |
28
28
  | E8 | Stub / placeholder code without tracking | Judge `pass`, `...`, and `raise NotImplementedError` directly; track follow-up work outside source code. |
29
- | E9 | Constants-module exports with no importer | A module-level `UPPER_SNAKE` constant added to a `*_constants.py` / `config/` module that no module in the repo imports and that the constants file itself never references. The file-global use-count gate exempts a constants module because every name it exports legitimately carries zero in-file references, so a genuinely dead export slips past the write-time gate. Distinguish dead from live by grepping the whole repo for each constant name: a sibling such as `MEDIUM_TERMINAL` imported by a consumer module is live; a `MEDIUM_TEXT` that no `from ... import` line and no in-file reference names is dead (CODE_RULES 9.8). Remove the dead export. |
29
+ | E9 | Constants-module exports with no importer | A module-level `UPPER_SNAKE` constant added to a `*_constants.py` / `config/` module that no module in the repo imports and that the constants file itself never references. No write-time check reads a constants module, and every name such a module exports carries zero in-file references by design, so a dead export reaches the audit untouched. Distinguish dead from live by grepping the whole repo for each constant name: a sibling such as `MEDIUM_TERMINAL` imported by a consumer module is live; a `MEDIUM_TEXT` that no `from ... import` line and no in-file reference names is dead (CODE_RULES 9.8). Remove the dead export. |
30
30
 
31
31
  ---
32
32
 
@@ -5,7 +5,7 @@
5
5
  **Examples of Category J findings:**
6
6
  - A literal `60` appears in a production function body (magic value rule).
7
7
  - A new `MAX_RETRIES = 3` declared at module scope outside `config/`.
8
- - A parameter named `ctx` instead of `context` (abbreviation rule).
8
+ - A parameter named `ctx` where the name `context` belongs (abbreviation rule).
9
9
  - A function that returns a value with no return-type annotation.
10
10
  - A new `# explains the loop logic` comment added to production code.
11
11
 
@@ -20,7 +20,6 @@
20
20
  | J1 | Magic values in production function bodies | Literals other than `0`, `1`, `-1` inside production function bodies. Test files exempt. |
21
21
  | J2 | String-template magic | f-strings whose structural literal text (paths, URLs, patterns) belongs in `config/`. |
22
22
  | J3 | Constants location | Module-level `UPPER_SNAKE = ...` outside `config/` in production code. Exempt path families: `config/*`, `/migrations/`, `/workflow/`, `_tab.py`, `/states.py`, `/modules.py`, test files. |
23
- | J4 | File-global use-count | A file-global constant referenced by fewer than two methods/functions/classes in the same file. |
24
23
  | J5 | Abbreviations | `ctx`, `cfg`, `msg`, `btn`, `idx`, `cnt`, `elem`, `val`, `tmp`, `str`, `num`, `arr`, `obj`, `fn`, `cb`, `req`, `res`. (Loop counters `i`/`j`/`k` and `e` for exceptions are exempt.) |
25
24
  | J6 | Vague names | `result`, `data`, `output`, `response`, `value`, `item`, `temp`, `info`, `stuff`, `thing`. Vague prefixes: `handle`, `process`, `manage`, `do`. |
26
25
  | J7 | Type hints | Missing type annotation on a parameter or return; presence of `Any` or `# type: ignore`. |
@@ -26,7 +26,7 @@ Inline the artifact under this section using the section types defined in the ch
26
26
 
27
27
  **E2. Functions / methods defined but never called**
28
28
  - For every function, method, or callable defined or modified by the artifact, enumerate call sites: direct calls, callback registrations (`onerror=`, `key=`, signal handlers), decorator applications, `__main__` guards, framework-driven discovery (pytest collection, Django URL resolution, FastAPI route decorators, Click groups), and string-form references resolved at runtime (`getattr`, dispatch tables).
29
- - Leading-underscore names: a single internal call site is sufficient; explicitly verify nothing outside the file imports the underscore-prefixed name.
29
+ - Leading-underscore names: a single internal call site is enough; explicitly verify nothing outside the file imports the underscore-prefixed name.
30
30
  - Public names: confirm at least one call site inside or outside the file, or confirm the function is part of a documented public API.
31
31
  - Framework-discovered callables (test functions, route handlers, CLI commands): state which collector picks them up and confirm the artifact's path matches that collector's pattern.
32
32
 
@@ -71,7 +71,7 @@ Inline the artifact under this section using the section types defined in the ch
71
71
 
72
72
  **E9. Constants-module exports with no importer**
73
73
  - For every module-level `UPPER_SNAKE` constant the artifact adds to a `*_constants.py` or `config/` module, grep the whole repo for the constant name and locate at least one importer (`from <module> import <NAME>`) or in-file reference.
74
- - The file-global use-count gate exempts a constants module because every name it exports carries zero in-file references by design, so a genuinely dead export slips past the write-time gate; this sub-bucket is the audit-time backstop for that exemption.
74
+ - Every name a constants module exports carries zero in-file references by design and no write-time check reads such a module, so a dead export reaches the audit untouched; this sub-bucket is the audit-time backstop.
75
75
  - A sibling that a consumer module imports is live; a constant that no `from ... import` line and no in-file reference names anywhere in the repo is dead and must be removed (CODE_RULES 9.8).
76
76
  - Constants reached only by string-form lookup (`getattr(config, name)`, settings registries) are live; name the dynamic consumer when you mark such a constant referenced.
77
77
  - Adversarial probes for proof-of-absence: (a) does the artifact add any constant to a `*_constants.py` / `config/` module whose name returns zero hits outside its own definition line? (b) is any newly added constant shadowed by a same-named constant in a sibling module so the importer resolves the other one? (c) does any constant exist only as an `__all__` re-export with no downstream importer of that re-export?
@@ -50,11 +50,6 @@ A `test_*.py` name or a `.mjs` extension takes the line out of the write-time ga
50
50
  - Distinguish *imports* (`from config.X import FOO`) from *declarations* — imports are not J3-scope.
51
51
  - Adversarial probes: (a) does any module-level constant masquerade as an "import" via a re-export pattern? (b) is there a `_PRIVATE_UPPER` declaration that escapes the visual UPPER_SNAKE filter but is still module-level? (c) does any test file declare a constant that *would* be flagged if it were in production, indicating the constant probably belongs in `config/` even if test-exempt?
52
52
 
53
- **J4. File-global use-count**
54
- - For every file-global constant outside `config/`, count references in the same file. Single ref → move to `config/`. Zero refs → delete.
55
- - The rule applies to *constants*, not functions, classes, or imports.
56
- - Adversarial probes: (a) is any imported constant referenced only once in the importing file (suggesting the import itself is gratuitous)? (b) is any helper function defined in a production file but never called from inside the same file (separate dead-code concern, surfaced here for completeness)? (c) does any constant in `config/` get imported from zero call sites across the repo?
57
-
58
53
  **J5. Abbreviations**
59
54
  - Walk every parameter, local, and attribute name across production, test, and JavaScript changed lines. Flag: `ctx`, `cfg`, `msg`, `btn`, `idx`, `cnt`, `elem`, `val`, `tmp`, `str`, `num`, `arr`, `obj`, `fn`, `cb`, `req`, `res`. Loop counters `i`/`j`/`k` and `e` for exceptions are exempt.
60
55
  - This audit walks changed test-file and `.mjs` / `.js` lines for this rule, even though the write-time hook skips them.
@@ -101,7 +96,7 @@ A `test_*.py` name or a `.mjs` extension takes the line out of the write-time ga
101
96
 
102
97
  ## Cross-bucket questions to answer at the end
103
98
 
104
- Q1: Are there literals or names that span two sub-buckets (e.g., a magic value in J1 that also appears inside an f-string scrutinized by J2; an UPPER_SNAKE in J3 that also fails the use-count test in J4)? Cite the literal/name and both sub-bucket IDs.
99
+ Q1: Are there literals or names that span two sub-buckets (e.g., a magic value in J1 that also appears inside an f-string scrutinized by J2; an UPPER_SNAKE in J3 that is also an abbreviation under J5)? Cite the literal/name and both sub-bucket IDs.
105
100
 
106
101
  Q2: What is the worst CODE_RULES drift introduced by this artifact? Cite `<file>:<line>`. (Common candidates: cross-language duplicates, stale help text mirroring a config constant, abbreviations in a public API surface, bare `Any` annotations, hardcoded user paths in installer scripts.)
107
102
 
@@ -149,13 +144,6 @@ ID prefix: `find`.
149
144
  - `Install-SweepEmptyDirs.ps1` line 245 — `$TaskName = "SweepEmptyDirs"` is a PowerShell variable; J3 enforces Python module-level UPPER_SNAKE outside `config/`. PowerShell out of scope for J3.
150
145
  - Adversarial probes: (a) does any module-level constant in `sweep_empty_dirs.py` masquerade as an "import"? (b) is there any `_PRIVATE_UPPER` declaration that escapes the visual UPPER_SNAKE filter? (c) does the test file accidentally declare a constant that *would* be flagged if it were in production?
151
146
 
152
- **J4. File-global use-count**
153
- - For every file-global constant outside `config/`, count references in the same file. Single ref → move to `config/`. Zero refs → delete.
154
- - `sweep_empty_dirs.py` — no file-global constants declared (`DEFAULT_AGE_SECONDS` and `DEFAULT_POLL_INTERVAL` are imports). Imports follow standard import-usage rules, not the file-global use-count rule.
155
- - `config/sweep_config.py:142-143` — `DEFAULT_AGE_SECONDS` and `DEFAULT_POLL_INTERVAL` live in `config/`, exempt by location.
156
- - Test file `_SCRIPTS_DIR` — test files exempt.
157
- - Adversarial probes: (a) is any imported constant in `sweep_empty_dirs.py` referenced only once (line 104 for `DEFAULT_AGE_SECONDS`, line 108 for `DEFAULT_POLL_INTERVAL`) — wait, `DEFAULT_AGE_SECONDS` is referenced TWICE on line 104 (`default=DEFAULT_AGE_SECONDS`) and line 105 (`f"...default: {DEFAULT_AGE_SECONDS} = 2 minutes"`); `DEFAULT_POLL_INTERVAL` is referenced TWICE on line 108 and line 109. Both meet the ≥2-references threshold (note: file-global use-count technically applies to declarations, not imports — listed here for completeness). (b) is any helper function in `sweep_empty_dirs.py` defined but never called from inside the file? `_log_walk_error` (line 73) is referenced once at line 84; `_build_parser` (line 101) is referenced once at line 114. The use-count rule applies to *constants*, not functions, so neither is flagged. (c) does any constant in `config/sweep_config.py` get imported from zero call sites?
158
-
159
147
  **J5. Abbreviations**
160
148
  - Walk every parameter, local, and attribute name in `sweep_empty_dirs.py`.
161
149
  - `_log_walk_error(os_error: OSError)` — `os_error` is full word, not `e` (exception loop var allowed but not required). Verified clean.
@@ -247,11 +235,9 @@ import time
247
235
  from config.sweep_config import DEFAULT_AGE_SECONDS
248
236
  from config.sweep_config import DEFAULT_POLL_INTERVAL
249
237
 
250
-
251
238
  def _log_walk_error(os_error: OSError) -> None:
252
239
  print(f"warning: cannot scan {os_error.filename} — {os_error.strerror}", file=sys.stderr)
253
240
 
254
-
255
241
  def sweep(root: str, min_age_seconds: int) -> list[str]:
256
242
  """Remove empty directories under *root* older than *min_age_seconds*."""
257
243
 
@@ -275,7 +261,6 @@ def sweep(root: str, min_age_seconds: int) -> list[str]:
275
261
 
276
262
  return removed
277
263
 
278
-
279
264
  def _build_parser() -> argparse.ArgumentParser:
280
265
  parser = argparse.ArgumentParser(description="Delete empty directories older than a given age.")
281
266
  parser.add_argument("root", help="Root directory to scan")
@@ -287,7 +272,6 @@ def _build_parser() -> argparse.ArgumentParser:
287
272
  help=f"Poll interval in seconds when looping (default: {DEFAULT_POLL_INTERVAL})")
288
273
  return parser
289
274
 
290
-
291
275
  def main() -> None:
292
276
  parser = _build_parser()
293
277
  arguments = parser.parse_args()
@@ -309,7 +293,6 @@ def main() -> None:
309
293
  print("
310
294
  stopped.")
311
295
 
312
-
313
296
  if __name__ == "__main__":
314
297
  main()
315
298
  ```
@@ -342,7 +325,6 @@ if str(_SCRIPTS_DIR) not in sys.path:
342
325
 
343
326
  from sweep_empty_dirs import sweep # noqa: E402
344
327
 
345
-
346
328
  def _set_creation_time_windows(path: str, timestamp: float) -> None:
347
329
  dt = datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)
348
330
  date_str = dt.strftime("%Y-%m-%d %H:%M:%S")
@@ -352,7 +334,6 @@ def _set_creation_time_windows(path: str, timestamp: float) -> None:
352
334
  check=True, capture_output=True,
353
335
  )
354
336
 
355
-
356
337
  def test_deletes_empty_dir_older_than_threshold() -> None:
357
338
  with tempfile.TemporaryDirectory() as tmp:
358
339
  empty_dir = os.path.join(tmp, "old_empty")
@@ -362,7 +343,6 @@ def test_deletes_empty_dir_older_than_threshold() -> None:
362
343
  assert empty_dir in removed
363
344
  assert not os.path.isdir(empty_dir)
364
345
 
365
-
366
346
  def test_skips_empty_dir_newer_than_threshold() -> None:
367
347
  with tempfile.TemporaryDirectory() as tmp:
368
348
  fresh_dir = os.path.join(tmp, "fresh_empty")
@@ -371,7 +351,6 @@ def test_skips_empty_dir_newer_than_threshold() -> None:
371
351
  assert fresh_dir not in removed
372
352
  assert os.path.isdir(fresh_dir)
373
353
 
374
-
375
354
  def test_deletes_nested_empty_dirs() -> None:
376
355
  with tempfile.TemporaryDirectory() as tmp:
377
356
  leaf = os.path.join(tmp, "parent", "child", "leaf")
@@ -384,13 +363,11 @@ def test_deletes_nested_empty_dirs() -> None:
384
363
  assert os.path.join(tmp, "parent", "child") in removed
385
364
  assert os.path.join(tmp, "parent") in removed
386
365
 
387
-
388
366
  def test_empty_root_does_not_crash() -> None:
389
367
  with tempfile.TemporaryDirectory() as tmp:
390
368
  _set_creation_time_windows(tmp, time.time() - 300)
391
369
  sweep(tmp, min_age_seconds=120)
392
370
 
393
-
394
371
  def test_skips_nonempty_dir() -> None:
395
372
  with tempfile.TemporaryDirectory() as tmp:
396
373
  nonempty_dir = os.path.join(tmp, "has_stuff")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.0.0",
3
+ "version": "8.1.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,12 +28,14 @@ When the code reduces each input to a canonical form and then compares the forms
28
28
 
29
29
  Name the degenerate value a dead implementation would return — empty string, `None`, `False`, a blanket refusal, an empty collection. When the test's expected value equals that default, the test passes whether the mechanism works or not. On its own it proves nothing.
30
30
 
31
- **Do this:** keep at least one case that expects the **non-default** answer, and drive the real code path — not a mock that only records that a call happened.
31
+ **Do this:** keep at least one case that expects the **non-default** answer, and drive the production code path — not a mock that only records that a call happened.
32
32
 
33
33
  ### 3. What single change to the code would make this test fail?
34
34
 
35
35
  When the honest answer is "none," or "only a change that also breaks everything else," the test is decoration. Drop it or rewrite it so one named mutation kills it.
36
36
 
37
+ **The policy-surface case.** A test that asserts a policy's own wording appears in the file that declares the policy has one mutation available: editing that sentence. It cannot report whether anything obeys the policy, so a suite full of such tests measures how many files repeat a rule, which is the duplication the reader was already complaining about. Assert instead that the surfaces the policy governs comply with it, and let the declaring file be the input rather than the subject.
38
+
37
39
  ## What a mechanism with a degenerate failure mode needs
38
40
 
39
41
  1. **At least one non-default case** that exercises the live path and expects the non-default answer.
@@ -0,0 +1,70 @@
1
+ # Archiving a Rule, Skill, Agent, or Command
2
+
3
+ **When this applies:** Pruning agent configuration in this package — moving a
4
+ rule, skill, agent, or command out of service, and deciding whether one is
5
+ still used at all.
6
+
7
+ ## Prove disuse before you remove
8
+
9
+ A prune is only as good as the usage evidence behind it, and three habits give
10
+ confident wrong answers here.
11
+
12
+ **Grepping the file's own name measures documentation, not use.** Enforcement in
13
+ this package is rarely named after the file that describes it. Resolve what the
14
+ file names instead: does the script exist, does the lint rule id appear in the
15
+ registry, does the check run. A rule with no mention of its slug anywhere can
16
+ still have a live checker behind it under a different name.
17
+
18
+ **Three files name every capability and prove nothing about any of them.**
19
+ `CHANGELOG.md` records history. The `README.md` inventory table lists what
20
+ ships. `bin/ever-shipped-skills.mjs` is a registry the prune reads, and it keeps
21
+ archived names on purpose. Subtract all three before you read a referrer count.
22
+
23
+ **A path this tree does not hold is not by itself a stale reference.** This
24
+ package installs into other repositories, so a rule or skill naming a file that
25
+ lives in the host repository is working as intended. Ask whether the specific
26
+ script or id it names resolves where it would run, not whether the path exists
27
+ here.
28
+
29
+ ## Archive rather than delete
30
+
31
+ Move the file with `git mv` into the archive sibling: `rules-archived/` beside
32
+ `rules/`, `.agents/skills-archived/` beside `.agents/skills/`. The archive is a
33
+ sibling and never a child, because the installer copies a content directory
34
+ whole.
35
+
36
+ Archives ship nothing by omission. `CONTENT_DIRECTORIES` in `bin/install.mjs`
37
+ names the directories the installer writes, and no archive sibling is on that
38
+ list; skills are enumerated from `.agents/skills` alone. So an archived file
39
+ leaves every install without any further step, and it comes back with the
40
+ reverse `git mv`.
41
+
42
+ Record a row in the archive manifest for each file: why it went, the `git mv`
43
+ that restores it, and every other edit a restore has to undo. A restore that has
44
+ to be reconstructed from the diff is not recoverable in any useful sense.
45
+
46
+ ## Make the archive stick
47
+
48
+ Three things do not follow the file on their own.
49
+
50
+ **A skill's name stays in `bin/ever-shipped-skills.mjs`.** The prune computes
51
+ the retired set as every name ever shipped minus the names installed now, and
52
+ that is how a stale copy is moved out of a host's agents home. Delete the name
53
+ and the copy is stranded on every machine that has it.
54
+
55
+ **Add the archived name to the banned active-capability list.** The reference
56
+ checker in `scripts/active_capability_references.py` does not resolve a named
57
+ capability against the shipped inventory; it reports a name only when that name
58
+ sits on `ALL_BANNED_ACTIVE_CAPABILITY_NAMES`. Until the archived name is on that
59
+ list, every leftover mention of it passes.
60
+
61
+ **A renamed skill directory takes its tests out of the run.** The node test
62
+ command globs `.agents/skills/**/*.test.mjs`, so a suite under an archived skill
63
+ stops running without failing. Check that the count you expect still runs.
64
+
65
+ ## Sibling rules
66
+
67
+ | Rule | Role |
68
+ |---|---|
69
+ | [`retired-hook-prose.md`](retired-hook-prose.md) | Prose names only hooks that run, and retiring a gate drops the detours it required |
70
+ | [`doc-inventory-integrity.md`](doc-inventory-integrity.md) | Inventory tables stay in step with what ships |
@@ -26,7 +26,7 @@ definitions and dictionary decisions.
26
26
  - Use inclusive, neutral language.
27
27
  - Use periods, commas, colons, and bullets to show structure.
28
28
  - Preserve exact quoted labels, identifiers, formulas, titles, and interface text.
29
- - Use `WARNING` for a real risk of injury or death. Use `CAUTION` for a real risk of equipment, tool, or machine damage. State the command or condition first, then state the result.
29
+ - Use `WARNING` for a risk of injury or death. Use `CAUTION` for a risk of equipment, tool, or machine damage. State the command or condition first, then state the result.
30
30
  - Aim for 20 words or fewer in a procedure sentence when the technical content allows.
31
31
  - Aim for 25 words or fewer in a descriptive sentence when the technical content allows.
32
32
 
@@ -37,6 +37,21 @@ Clone the revision the workflow pins, then point the gate at that clone. That
37
37
  run asks CI's question and its answer carries. Report a local result by naming
38
38
  the revision it used, so a reader can tell which question it answered.
39
39
 
40
+ The selection flag decides which question the staged policy lint answers.
41
+ `--staged` and `--base <revision>` carry each file's prior text, so the lint
42
+ subtracts what the prior text already reported and only a breach the change
43
+ introduced survives. `--files` and `--repository` carry no prior text, so every
44
+ breach in the file reports and the command exits non-zero on debt the change
45
+ never touched. CI runs the merge-base form, so reproduce a CI verdict with it:
46
+
47
+ ```
48
+ git merge-base HEAD origin/main
49
+ python packages/claude-dev-env/scripts/cde_lint.py --base <the revision that printed>
50
+ ```
51
+
52
+ A `--files` run that comes back red on a file you touched has answered a
53
+ different question. Read the reported line before you treat it as yours.
54
+
40
55
  ## The verdict belongs to CI
41
56
 
42
57
  CI decides whether a change passed, from evidence CI gathered. Keep that loop
@@ -7,7 +7,7 @@ Source: [Anthropic — Reduce file creation in agentic coding](https://platform.
7
7
  ## During a task
8
8
 
9
9
  - Prefer working in memory over creating scratchpad files. Use variables and tool results instead of writing intermediate data to disk.
10
- - When a temporary file is genuinely needed (e.g., a helper script, a test fixture, a debug output), track it mentally for cleanup.
10
+ - When a temporary file is needed (e.g., a helper script, a test fixture, a debug output), track it mentally for cleanup.
11
11
 
12
12
  ## When a task is complete
13
13
 
@@ -11,7 +11,7 @@
11
11
  | Contract | `docs/CODE_RULES.md` | Full review criteria for PR agents, loaded on demand |
12
12
  | Pointer | `.cursor/BUGBOT.md` | Checked-in file Cursor BugBot reads; points at `CODE_RULES.md` |
13
13
  | Enforcer | `hooks/blocking/code_rules_enforcer.py` | Hand-maintained checks the staged policy lint runs; not generated from the docs |
14
- | Lint | `scripts/cde_lint.py` | Runs the enforcer and the other policy rules over staged or changed files |
14
+ | Lint | `scripts/cde_lint.py` | Runs the enforcer and the other policy rules over staged or changed files, grading each against the file's prior text; see [`ci-owns-the-gate.md`](ci-owns-the-gate.md) for what each selection flag reports |
15
15
  | Session rules | `rules/*.md` | Runtime session policy (questions, tasks, shell) |
16
16
 
17
17
  Load `CODE_RULES.md` when reviewing a PR, resolving a policy conflict, or generating code. Prefer linking this ref over restating rules.
@@ -19,7 +19,7 @@ Load `CODE_RULES.md` when reviewing a PR, resolving a policy conflict, or genera
19
19
  Two standards live in `CODE_RULES.md` in full:
20
20
 
21
21
  - **TDD** — CODE_RULES §8 / AGENTS Tests: red, green, refactor; no production code before a failing test.
22
- - **Right-sized engineering** — CODE_RULES §7 / AGENTS Design: functions over classes; concrete over abstract; add an abstraction at the commit that introduces its second concrete implementation.
22
+ - **Right-sized engineering** — CODE_RULES §7 / AGENTS Design: functions over classes; concrete over abstract; add an abstraction at the commit that introduces its second concrete implementation. That count is a house call, one occurrence earlier than the rule of three Fowler credits to Don Roberts. The direction comes from the literature; the number does not, so read it as this package's setting rather than as a cited standard.
23
23
 
24
24
  BDD is the outer process and TDD is the inner loop: [`bdd.md`](bdd.md) discovers and formulates the behavior a feature needs, then each formulated behavior is built through the TDD cycle.
25
25
 
@@ -36,7 +36,7 @@ A hook module is the sharpest case of the same rule: its docstring lead narrativ
36
36
 
37
37
  The staged policy lint carries this as its `hook-prose-consistency` rule, covering hook modules and their `*_constants.py` companions. It reports prose that claims a trigger the detector never fires on, and names the fix. No write-time hook runs it, so CI is where it reports.
38
38
 
39
- After writing a hook, ask: would a token matching every word of this message actually trip the detector? When the message names a shape the regex skips, rewrite the message to name only what the regex catches. The path-shape case is the common overstatement — a detector that keys off a path separator must not claim it blocks an "output-key segment". The corrective message spells the rewrite.
39
+ After writing a hook, ask: would a token matching every word of this message trip the detector? When the message names a shape the regex skips, rewrite the message to name only what the regex catches. The path-shape case is the common overstatement — a detector that keys off a path separator must not claim it blocks an "output-key segment". The corrective message spells the rewrite.
40
40
 
41
41
  ## Full standard
42
42
 
@@ -13,7 +13,7 @@ Note: This deliberately chooses exploration depth over the "commit and execute q
13
13
 
14
14
  ## Exploration scales with risk
15
15
 
16
- - Small change to a familiar file: a quick read of the file and its immediate neighbors is sufficient.
16
+ - Small change to a familiar file: a quick read of the file and its immediate neighbors is enough.
17
17
  - New feature or cross-cutting change: read broadly across the codebase to understand how similar things are done.
18
18
  - Architectural decision: explore the full landscape before recommending a direction.
19
19
 
@@ -23,4 +23,4 @@ The depth budget shrinks once the evidence is in hand. When you can already name
23
23
 
24
24
  ## Relationship to other rules
25
25
 
26
- - **research-mode.md** ensures factual claims are grounded. This rule ensures implementation plans are grounded in the actual codebase.
26
+ - **research-mode.md** ensures factual claims are grounded. This rule ensures implementation plans are grounded in the codebase.
@@ -47,7 +47,7 @@ The re-raise comes first, sending an escalation directly through the boundary.
47
47
  An agent that hits a member failure keeps working the problem. Three things bound how:
48
48
 
49
49
  - **Repair in place.** The current run preserves every completed member.
50
- - **Three real attempts, then park.** An attempt is a theory of the cause, acted on. Re-running the same code on the same input counts as one attempt. Three theories cover the obvious cause, the second guess, and the cause revealed by the first two attempts. Judgment selects each theory; the third attempt sets the stopping point. After the third theory fails, park the member with its reason and move to the next. Parked members return after the batch.
50
+ - **Three attempts, then park.** An attempt is a theory of the cause, acted on. Re-running the same code on the same input counts as one attempt. Three theories cover the obvious cause, the second guess, and the cause revealed by the first two attempts. Judgment selects each theory; the third attempt sets the stopping point. After the third theory fails, park the member with its reason and move to the next. Parked members return after the batch.
51
51
  - **The batch always reaches a deliverable.** Complete every member that can complete, produce the packaged artifact, then work the parked list. A run with 34 of 37 members complete and 3 parked records progress and continues to delivery.
52
52
 
53
53
  ## Three alike means one cause
@@ -58,9 +58,9 @@ The run report groups parked members by that signature and names every group of
58
58
 
59
59
  ## Close the run with every outcome
60
60
 
61
- Every issue the run hit gets one line in the closing report: what failed, and how it ended — repaired, worked around, or parked. Members that finished after a repair belong in that list beside the parked ones. A workaround patched past mid-run is the likeliest real defect in the batch, because the closing report becomes its durable record.
61
+ Every issue the run hit gets one line in the closing report: what failed, and how it ended — repaired, worked around, or parked. Members that finished after a repair belong in that list beside the parked ones. A workaround patched past mid-run is the likeliest defect in the batch, because the closing report becomes its durable record.
62
62
 
63
- The report then presents each candidate to the owner for a durable-fix decision, names the fixes the run recommends, and waits for the answer. The next run builds the selected fix; the current run completes its deliverable first.
63
+ The report then presents each candidate to the owner for a durable-fix decision and names the fix the run recommends for each one. The report is a statement, not a gate: the current run completes its deliverable and ends. A later run builds whichever fixes the owner selects, and the recommendation stands as the default for any the owner does not rule on.
64
64
 
65
65
  ## Enforcement
66
66
 
@@ -82,7 +82,7 @@ scope, and the deliverable remains the run priority.
82
82
 
83
83
  Repair in place and preserve every completed asset in the current run.
84
84
 
85
- Three real attempts, then park. An attempt is a theory of the cause, acted
85
+ Three attempts, then park. An attempt is a theory of the cause, acted
86
86
  on. Repeated execution of one theory remains one attempt.
87
87
  Three theories cover the obvious cause, the second guess, and the cause
88
88
  revealed by the first two attempts. Use your judgment to select each theory;
@@ -108,11 +108,12 @@ stops. For an asset-level stop, put the handling inside the loop body.
108
108
  Close the run by reporting what broke and what you did about it. Every issue
109
109
  gets one line: what failed, and how it ended — repaired, worked around, or
110
110
  parked. Include the ones you solved; a workaround you patched past in attempt
111
- two is the likeliest real defect in the list, because the closing report becomes
111
+ two is the likeliest defect in the list, because the closing report becomes
112
112
  its durable record.
113
- Present each candidate for the owner's durable-fix decision, state which fixes
114
- you recommend and why, and wait for the answer. The next run builds the
115
- selected durable fix after this run completes its deliverable.
113
+ Present each candidate for the owner's durable-fix decision and state which
114
+ fix you recommend for each and why. Do not wait on the answer. Complete this
115
+ run's deliverable and end. A later run builds whichever fixes the owner
116
+ selects, and your recommendation stands as the default for the rest.
116
117
 
117
118
  Report as: N of M complete, K parked, and what you are working now.
118
119
  Close with: what broke, how each one ended, and which of them deserve a
@@ -64,10 +64,13 @@ contexts appears on that level's head. A required check that never fired is
64
64
  invisible debt at every level, and it surfaces only after the whole stack is
65
65
  pushed, when the repair costs a second pass over every branch.
66
66
 
67
- A red required check blocks the branch, whoever owns the failing line. The gate
68
- charges a change for the whole file it touches, so "pre-existing on main" is a
69
- blame note, not a pass. Fix the line in the next push or report the branch
70
- blocked. Read the gate's own report rather than a narrower substitute. A
67
+ A red required check blocks the branch, whoever owns the failing line. The
68
+ staged policy lint grades a change against the file's prior text, so a finding
69
+ that survives is one the change introduced or made worse. Fix that line in the
70
+ next push or report the branch blocked. A finding the change did not introduce
71
+ is a gate-scoping defect: report it against the lint and leave the file's shape
72
+ alone. Restructuring a file to satisfy a mis-scoped check trades one finding for
73
+ a set of new ones. Read the gate's own report rather than a narrower substitute. A
71
74
  single-file mypy call cannot see sibling modules and reports false import
72
75
  errors, so it neither clears nor convicts a change.
73
76
 
@@ -12,7 +12,9 @@ This is the autonomous-run partner to the ambiguous case: research and recommend
12
12
 
13
13
  ## Do not end a turn on a promise
14
14
 
15
- Pause for the user only when the work truly needs them: a destructive or irreversible action, a real scope change, or input only they can give. When you hit one, ask through `AskUserQuestion` and end the turn. Do not end on a promise about work you have not done.
15
+ Pause for the user only when the work truly needs them: a destructive or irreversible action, a scope change, or input only they can give. When you hit one, ask through `AskUserQuestion` and end the turn. Do not end on a promise about work you have not done.
16
+
17
+ A pause is a request you deliver, not a state you enter. Put it in the channel the user reads, name the one input you need and what resumes the moment it arrives, and shape it so a single word answers it. A decision recorded in a status line, a checklist, a working document, or a side thread is a note; the user never learns they are the bottleneck, and the work sits. Keep everything that does not depend on the answer moving while it is outstanding.
16
18
 
17
19
  Before you end any turn, read your last paragraph. If it is a plan, an analysis, a list of next steps, or a statement of intent ("I'll run the tests", "next I'll wire it up"), do that work with tool calls before you stop. End the turn only when the task is done or you are blocked on input only the user can give.
18
20
 
@@ -25,5 +25,5 @@ Host, ssh port and ssh user are constants inside the runner and are also under t
25
25
 
26
26
  To copy a file, write it inside the script with a quoted heredoc rather than reaching for `scp`.
27
27
 
28
- The `/tmp` size limit, how to tell a real break from a platform-only one, and a failure-to-cause
28
+ The `/tmp` size limit, how to tell a break in the code from a platform-only one, and a failure-to-cause
29
29
  table: `@~/.claude/docs/nas-ssh-invocation.md`.
@@ -16,8 +16,10 @@ Every recommendation, claim, or piece of advice must cite a specific source:
16
16
 
17
17
  If you generate a claim and cannot find a supporting source, retract it. Do not present it.
18
18
 
19
+ A citation is checked against what the source says, not against whether the source exists. A named authority attached to a claim the authority does not make is weaker than no citation at all, because it stops the reader from asking. This bites hardest on numbers: a threshold that runs stricter or looser than its source is a house call, so label it as one and leave the attribution to the direction the source does support.
20
+
19
21
  ## 3. Direct quotes for factual grounding
20
- When working from documents, extract the actual text first before analyzing. Ground your response in word-for-word quotes, not paraphrased summaries. Reference the quote when making your point.
22
+ When working from documents, extract the text first before analyzing. Ground your response in word-for-word quotes, not paraphrased summaries. Reference the quote when making your point.
21
23
 
22
24
  ## How citations appear in a chat reply
23
25
 
@@ -5,12 +5,20 @@ paths:
5
5
 
6
6
  # Rules Prose Names Only Hooks That Run
7
7
 
8
- A hook module runs when `hooks/hooks.json` registers it or a dispatcher roster hosts it. A module can survive on disk while appearing in neither place, and it then reaches no tool call. Prose that gives such a module a present-tense action describes a gate the reader never faces, and a reader who trusts it skips the check they were meant to run by hand.
8
+ A hook module runs when `hooks/hooks.json` registers it or a dispatcher roster hosts it. A module can survive on disk while appearing in neither place, and it then reaches no tool call. Presence on disk settles nothing in either direction. The installer writes an inert stand-in at a retired hook path when a host's stale `settings.json` still names it, because a registered path that resolves to nothing makes the interpreter exit 2 and the harness reads a PreToolUse exit 2 as a block. A retired hook can also keep working through a different door: its logic moves into a `*_parts/` package that a live module imports, so the hook name is dead while the check it carried still fires. Prose that gives such a module a present-tense action describes a gate the reader never faces, and a reader who trusts it skips the check they were meant to run by hand.
9
9
 
10
- The staged policy lint carries this check as its `retired-hook-prose` rule. It reads each Markdown file under a `rules/` directory, takes every backticked hook module name, and reports the ones that are registered nowhere and that carry a present-tense action verb in the rest of the sentence. CI runs that lint against the merge base.
10
+ The staged policy lint carries this check as its `retired-hook-prose` rule. It reads each Markdown file under a `rules/` directory, takes every backticked hook module name, and reports the ones that are registered nowhere and that carry a present-tense action verb in the rest of the sentence. CI runs that lint against the merge base. Its reach stops at that directory. Skill manifests, agent definitions, commands, `docs/` and `_shared/` all carry prose about hooks and none of it is read, so a stale claim there passes CI and reaches every reader. Sweep those trees by hand against the retired roster whenever you retire a hook.
11
11
 
12
12
  A module name counts as a hook when it ends in one of the hook families, such as `_blocker`, `_enforcer`, `_gate`, `_tracker`, or `_dispatcher`, and when it either sits under `hooks/` or appears in `RETIRED_HOOK_REGISTRATION_RELATIVE_PATHS` in `bin/install.mjs`. A support module inside the hooks tree carries no such family suffix, so it stays out of the check. A hook deleted from disk without an entry in that roster resolves nowhere, so add the roster entry when you retire a hook.
13
13
 
14
14
  Two shapes pass. A past-tense sentence that records what a hook once did makes no live claim. A sentence that names the staged policy lint, `cde_lint`, or a repository check tells the reader where the work runs now, so the module name reads as a pointer to code.
15
15
 
16
+ ## Retiring a gate drops the detours it required
17
+
18
+ A gate and the standing orders written to satisfy it are one unit. An order that says to spawn a named agent first, to carry a magic token into a spawn, or to route through an extra step exists because some check demanded it. When the check goes, that order is a toll every later session pays for nothing, and it costs more than a stale sentence does, because a reader obeys it.
19
+
20
+ So retiring a hook is finished only when the detours it required are gone in the same change. Search for the order, not for the module: the module name is what the lint looks for, and a detour usually names the agent, the step, or the token instead, which is how these survive a lint that reads the same file. Drop the requirement and keep the agent, the skill, or the command itself; a capability nobody is forced to call costs nothing, while a forced call costs every run.
21
+
22
+ The same duty covers every lane a withdrawn standard rode on. A threshold enforced by a hook is often also stated in an audit rubric, an agent's instruction table, or a review prompt. Withdraw all of them together, and name the lanes in the archive entry. Removing the fast mechanical lane and leaving the slow judgment lane keeps the standard in force while deleting the text that a reader could argue with.
23
+
16
24
  When the rule reports a line, say what the reader faces now. Name the harness permission prompt, the staged policy lint, or the repository check that carries the work, or drop the claim.
package/rules/testing.md CHANGED
@@ -14,14 +14,14 @@ paths:
14
14
 
15
15
  ## Complete Mocks for Testability
16
16
 
17
- **Mocks must include all fields the component actually uses.**
17
+ **Mocks must include all fields the component uses.**
18
18
 
19
19
  If a component renders field X, the mock must have field X with a valid value.
20
20
  Incomplete mocks make it impossible to distinguish "broken code" from "missing data".
21
21
 
22
- ## Tests Exercise Real Behavior
22
+ ## Tests Exercise Production Behavior
23
23
 
24
- Tests exercise real behavior, real data, and production code paths. A test that asserts on a stand-in for the production path proves the stand-in works.
24
+ Tests exercise production behavior, production data, and production code paths. A test that asserts on a stand-in for the production path proves the stand-in works.
25
25
 
26
26
  ## No Gate Holds the Test-First Order
27
27
 
@@ -19,6 +19,7 @@ Before writing any AskUserQuestion or asking a clarifying question in chat, eval
19
19
  | Is the answer determined by file naming patterns or contents? | Glob a sample and inspect. |
20
20
  | Is the answer a value in a config or environment variable? | Read the config or check the env. |
21
21
  | Is the answer retrievable from any available MCP tool? | Use the tool. |
22
+ | Did the user already state a criterion, standard, or line that decides this? | Apply it, state the call and the reason, and keep going. |
22
23
 
23
24
  Only after confirming the answer cannot be obtained through any available tool, ask the user.
24
25
 
@@ -37,6 +38,8 @@ Reserve user questions for:
37
38
  - **Judgment calls** — tradeoffs the user needs to evaluate.
38
39
  - **Scope decisions** — what to include or exclude from a piece of work.
39
40
 
41
+ A preference question is one where the user has not yet given the line. Once they have, every case under that line is yours to decide. Handing back each application of a stated criterion turns one decision into many and stalls the work, because the person who set the standard does not hold the individual answers. Apply the criterion, name the call and the reason it went that way, and escalate only what the criterion cannot settle: a new axis it never covered, or a step nobody can undo. [`long-horizon-autonomy.md`](long-horizon-autonomy.md) carries the same duty for authority the task already granted.
42
+
40
43
  ## Examples
41
44
 
42
45
  **Wrong:** "Are there multiple images per folder, or just one image + one mp4?"
@@ -23,7 +23,7 @@ Before stating a runtime claim, gather the matching live signal:
23
23
  | The server is up | Probe the port; a refused connection means it is down. |
24
24
  | The process is running | List processes and match the name or PID. |
25
25
  | The change took effect | Drive the flow and watch the new behavior. A script that proves a branch's behavior names the module file it loaded, as its first step, because an installed copy of the same package shadows the checkout you meant to test. |
26
- | The dependency is reachable | Send one real request and read the response. |
26
+ | The dependency is reachable | Send one request and read the response. |
27
27
  | The release or deploy shipped | Read what it makes: the tag, the published version, the file on disk. |
28
28
  | The pipeline did the work | Read each job's own result. A run reports success while a job inside it is skipped. |
29
29
  | The automation works | Drive the branch that matters. A green run of the do-nothing branch proves nothing. |
@@ -39,4 +39,4 @@ Only after a live signal backs the claim do you state it.
39
39
  **Right:** Tail the request log and confirm the retry fired, or report that no retry line appears.
40
40
 
41
41
  **Wrong:** "The config sets the timeout to 30 seconds, so the timeout is fine."
42
- **Right:** Print the loaded config at runtime and report the value the process actually holds.
42
+ **Right:** Print the loaded config at runtime and report the value the process holds.
@@ -826,7 +826,7 @@ def test_failure_blast_radius_projection_uses_the_canonical_excerpt() -> None:
826
826
  projected_instruction = materializer.render_codex_failure_blast_radius(canonical_rule)
827
827
 
828
828
  assert projected_instruction.startswith("Failure handling for this run")
829
- assert "Three real attempts, then park." in projected_instruction
829
+ assert "Three attempts, then park." in projected_instruction
830
830
  assert "Report as: N of M complete" in projected_instruction
831
831
 
832
832
 
@@ -851,7 +851,7 @@ def test_build_plan_publishes_owned_agents_projection_and_tracks_drift(
851
851
  assert manifest_record["source"] == "rules/failure-blast-radius.md"
852
852
 
853
853
  rule_path.write_text(
854
- rule_path.read_text(encoding="utf-8").replace("Three real attempts, then park.", "Three tested attempts, then park."),
854
+ rule_path.read_text(encoding="utf-8").replace("Three attempts, then park.", "Three tested attempts, then park."),
855
855
  encoding="utf-8",
856
856
  )
857
857
  drifted_plan, _ = build_plan(config, all_agents=[])
@@ -1021,7 +1021,7 @@ def _assert_projection_and_manifest(
1021
1021
  ) -> None:
1022
1022
  assert not retired_owned_path.exists()
1023
1023
  projected_instruction = (target / "AGENTS.md").read_text(encoding="utf-8")
1024
- assert "Three real attempts, then park." in projected_instruction
1024
+ assert "Three attempts, then park." in projected_instruction
1025
1025
  manifest = json.loads((target / "hooks.json").read_text(encoding="utf-8"))
1026
1026
  assert manifest["hooks"]["PreToolUse"][0]["matcher"] == "apply_patch"
1027
1027
  assert manifest["hooks"]["PreToolUse"][1] == existing_hooks["hooks"]["PreToolUse"][1]