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.
- package/audit-rubrics/audit-categories.json +0 -4
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -1
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -2
- package/audit-rubrics/prompts/category-e-dead-code.md +2 -2
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +1 -24
- package/package.json +1 -1
- package/rules/anti-corollary-tests.md +3 -1
- package/rules/archiving-agent-config.md +70 -0
- package/rules/asd-ste100-language.md +1 -1
- package/rules/ci-owns-the-gate.md +15 -0
- package/rules/cleanup-temp-files.md +1 -1
- package/rules/code-standards.md +2 -2
- package/rules/docstring-prose-matches-implementation.md +1 -1
- package/rules/explore-thoroughly.md +2 -2
- package/rules/failure-blast-radius.md +9 -8
- package/rules/git-workflow.md +7 -4
- package/rules/long-horizon-autonomy.md +3 -1
- package/rules/nas-ssh-invocation.md +1 -1
- package/rules/research-mode.md +3 -1
- package/rules/retired-hook-prose.md +10 -2
- package/rules/testing.md +3 -3
- package/rules/verify-before-asking.md +3 -0
- package/rules/verify-runtime-state.md +2 -2
- package/scripts/tests/test_codex_compat_materializer.py +3 -3
|
@@ -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.
|
|
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`
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
package/rules/code-standards.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
114
|
-
you recommend and why
|
|
115
|
-
|
|
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
|
package/rules/git-workflow.md
CHANGED
|
@@ -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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
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
|
|
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`.
|
package/rules/research-mode.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
22
|
+
## Tests Exercise Production Behavior
|
|
23
23
|
|
|
24
|
-
Tests exercise
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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]
|