claude-dev-env 8.15.0 → 8.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/.agents/agents/test_agent_frontmatter.py +0 -72
  2. package/.agents/agents-archived/code-quality-agent.md +16 -16
  3. package/.agents/skills/_shared/end-of-run-gotcha-recommendations.md +5 -21
  4. package/.agents/skills/e-code-review/SKILL.md +1 -3
  5. package/.agents/skills/e-simplify/SKILL.md +6 -6
  6. package/.agents/skills/eli5/SKILL.md +4 -14
  7. package/.agents/skills/eli5/reference/task-seeds.md +3 -4
  8. package/.agents/skills/everything-search/SKILL.md +2 -2
  9. package/.agents/skills/fresh-branch/SKILL.md +2 -3
  10. package/.agents/skills/grok-spawn/SKILL.md +3 -8
  11. package/.agents/skills/issue-tracker/SKILL.md +4 -6
  12. package/.agents/skills/orchestrator-refresh/SKILL.md +7 -7
  13. package/.agents/skills/plugin-eval-standalone-skill/SKILL.md +3 -3
  14. package/.agents/skills/privacy-hygiene/SKILL.md +6 -10
  15. package/.agents/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
  16. package/.agents/skills/pull-request/SKILL.md +11 -14
  17. package/.agents/skills/pull-request/reference/publication-tasks.md +2 -4
  18. package/.agents/skills/recovering-codex-startup/SKILL.md +3 -6
  19. package/.agents/skills/recovering-codex-startup/reference/task-seeds.md +1 -1
  20. package/.agents/skills/repairing-hook-boundaries/SKILL.md +4 -7
  21. package/.agents/skills/repairing-hook-boundaries/reference/task-seeds.md +1 -2
  22. package/.agents/skills/second-account-workers/SKILL.md +26 -8
  23. package/.agents/skills/syncing-submodules/SKILL.md +7 -17
  24. package/.agents/skills/syncing-submodules/reference/run-tasks.md +1 -1
  25. package/.agents/skills/task-build/SKILL.md +1 -1
  26. package/.agents/skills/task-build/reference/tool-routing.md +1 -1
  27. package/.agents/skills/team-advisor/SKILL.md +2 -4
  28. package/.agents/skills/usage-pause/SKILL.md +1 -1
  29. package/.agents/skills/windows-scheduled-task/SKILL.md +1 -1
  30. package/AGENTS.md +1 -1
  31. package/_shared/advisor/advisor-protocol.md +4 -4
  32. package/_shared/advisor/reference/consult-format.md +1 -1
  33. package/_shared/advisor/reference/identity.md +2 -2
  34. package/_shared/advisor/reference/warm-up.md +1 -1
  35. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
  36. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -16
  37. package/bin/install.mjs +1 -0
  38. package/bin/install.prompt-context.test.mjs +7 -3
  39. package/bin/install.test.mjs +3 -2
  40. package/commands/sr-loop.md +2 -2
  41. package/docs/CODE_RULES.md +2 -2
  42. package/docs/DJANGO_PATTERNS.md +3 -3
  43. package/docs/REACT_PATTERNS.md +6 -6
  44. package/docs/TEST_QUALITY.md +10 -16
  45. package/docs/agent-spawn-protocol.md +4 -4
  46. package/docs/codex-compatibility.md +4 -33
  47. package/docs/references/team-advisor-skill.md +0 -1
  48. package/docs/worker-completion-gate.md +0 -1
  49. package/hooks/advisory/migration_safety_advisor.py +3 -2
  50. package/hooks/advisory/refactor_guard.py +3 -3
  51. package/hooks/hooks.json +1 -1
  52. package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +1 -2
  53. package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +1 -2
  54. package/package.json +1 -1
  55. package/rules/explore-thoroughly.md +0 -1
  56. package/rules/filesystem-search.md +1 -1
  57. package/rules/git-workflow.md +5 -5
  58. package/rules/long-horizon-autonomy.md +1 -5
  59. package/rules/re-stage-before-commit.md +0 -2
  60. package/rules/shell-invocation.md +1 -1
  61. package/scripts/claude_account_choice.py +183 -52
  62. package/scripts/claude_account_profile.py +81 -49
  63. package/scripts/claude_account_worker.py +48 -15
  64. package/scripts/dev_env_scripts_constants/claude_account_constants.py +36 -1
  65. package/scripts/dev_env_scripts_constants/claude_account_worker_constants.py +2 -0
  66. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +8 -2
  67. package/scripts/policy_lint/config/constants.py +1 -0
  68. package/scripts/policy_lint/engine.py +5 -2
  69. package/scripts/repository_checks/config/constants.py +1 -1
  70. package/scripts/spawn_grok_batch.py +12 -1
  71. package/scripts/test_claude_account_choice.py +127 -17
  72. package/scripts/test_claude_account_profile.py +63 -16
  73. package/scripts/test_claude_account_worker.py +118 -0
  74. package/scripts/test_spawn_grok_batch.py +22 -0
  75. package/scripts/tests/test_banned_prose_words.py +1 -1
  76. package/scripts/tests/test_policy_lint_rules_archive.py +16 -0
  77. package/scripts/tests/test_policy_lint_rules_engine.py +2 -2
  78. package/scripts/tests/test_rules.py +9 -9
  79. package/system-prompts/software-engineer.xml +15 -30
  80. package/audit-rubrics/audit-categories.json +0 -700
  81. package/audit-rubrics/category_rubrics/category-a-api-contracts.md +0 -90
  82. package/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +0 -36
  83. package/audit-rubrics/category_rubrics/category-c-resource-cleanup.md +0 -35
  84. package/audit-rubrics/category_rubrics/category-d-scoping-and-ordering.md +0 -35
  85. package/audit-rubrics/category_rubrics/category-e-dead-code.md +0 -40
  86. package/audit-rubrics/category_rubrics/category-f-silent-failures.md +0 -41
  87. package/audit-rubrics/category_rubrics/category-g-bounds-and-overflow.md +0 -38
  88. package/audit-rubrics/category_rubrics/category-h-security-boundaries.md +0 -40
  89. package/audit-rubrics/category_rubrics/category-i-concurrency.md +0 -38
  90. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +0 -59
  91. package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +0 -66
  92. package/audit-rubrics/category_rubrics/category-l-behavior-equivalence.md +0 -48
  93. package/audit-rubrics/category_rubrics/category-m-producer-consumer-cardinality.md +0 -44
  94. package/audit-rubrics/category_rubrics/category-n-test-name-scenario-verifier.md +0 -54
  95. package/audit-rubrics/category_rubrics/category-p-name-vs-behavior-contract.md +0 -59
  96. package/audit-rubrics/category_rubrics/category-q-cross-surface-claims.md +0 -46
  97. package/audit-rubrics/prompts/category-a-api-contracts.md +0 -407
  98. package/audit-rubrics/prompts/category-b-selector-engine-compat.md +0 -401
  99. package/audit-rubrics/prompts/category-c-resource-cleanup.md +0 -420
  100. package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +0 -414
  101. package/audit-rubrics/prompts/category-e-dead-code.md +0 -434
  102. package/audit-rubrics/prompts/category-f-silent-failures.md +0 -432
  103. package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +0 -383
  104. package/audit-rubrics/prompts/category-h-security-boundaries.md +0 -423
  105. package/audit-rubrics/prompts/category-i-concurrency.md +0 -429
  106. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +0 -458
  107. package/audit-rubrics/prompts/category-k-codebase-conflicts.md +0 -338
  108. package/audit-rubrics/prompts/category-l-behavior-equivalence.md +0 -131
  109. package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +0 -129
  110. package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +0 -139
  111. package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +0 -89
  112. package/audit-rubrics/prompts/category-q-cross-surface-claims.md +0 -51
  113. package/hooks/hooks_constants/style_reminder_prompt_constants.py +0 -9
  114. package/hooks/session/style_reminder_prompt.py +0 -55
  115. package/hooks/session/test_style_reminder_prompt.py +0 -86
@@ -1,44 +0,0 @@
1
- # Category M — Producer/consumer cardinality vs collection-type contract
2
-
3
- **What this category audits:** functions returning `list[X]`, `Sequence[X]`, or `Iterable[X]` where the producer can emit duplicates but the consumer treats the value as a set. Common when a subprocess-stdout parser walks every output line, when a registry query returns rows with non-unique keys, when a recursive walker re-enters the same node via two paths, or when a stream-fold accumulates without dedup. The bug surfaces downstream as `RuntimeError: duplicate key`, as a UI showing the same item twice, or as a writeback that re-applies the same operation.
4
-
5
- **Why this category is its own bucket:** Categories A–K catch failure modes in either the producer or the consumer in isolation. Category M catches the contract drift between them: the producer's return type promises `list[X]` (cardinality unconstrained), but a consumer downstream calls `set(result)`, builds a `dict.fromkeys(result, ...)`, or feeds the result into an `INSERT ... ON CONFLICT` that crashes on duplicates. The producer and consumer each look correct individually; the bug emerges only when their cardinality contracts disagree.
6
-
7
- **Examples of Category M findings:**
8
- - `_extract_paths_from_everything_cli_stdout` returns `list[Path]` but the consumer runs one `INSERT` per element against a `UNIQUE(path)` table, raising `sqlite3.IntegrityError: UNIQUE constraint failed` when the subprocess emits the same path twice. (pa#143 F10)
9
- - A database query returns duplicate `content_id` rows; the writeback path submits the same content twice and the second `INSERT` fails with a constraint violation. (pa#136 F30)
10
- - A writeback ignores the `content_id` key and re-applies an `UPDATE` against the same row, masking which row "won". (pa#136 F32)
11
- - A logger flushes every accumulator line without dedup; the same warning appears N times in the user-facing report.
12
-
13
- **Companion reference:** see `../source-material-section-types.md`.
14
-
15
- ---
16
-
17
- ## Sub-bucket decomposition (Category M)
18
-
19
- Decomposition is by the **kind of producer/consumer pair** whose cardinality contracts must agree.
20
-
21
- | ID | Axis name | Concrete checks |
22
- |---|---|---|
23
- | M1 | Subprocess-stdout parsers | Functions that walk lines from `subprocess.run(...).stdout` MUST return `frozenset[X]`, `dict.fromkeys`-deduplicated `list[X]`, OR carry explicit "duplicates preserved" docstring text — never bare `list[X]` |
24
- | M2 | Database / registry queries | Functions that build a `list[Row]` from a query MUST dedup by primary key when the consumer treats rows as a set, OR document "all rows returned, including duplicates" |
25
- | M3 | Consumer-expects-set anti-pattern | Consumer calls `set(producer())`, `dict.fromkeys(producer())`, `dict((k, v) for k, v in producer())`, or `INSERT ON CONFLICT` on the producer's output — this is a sign the producer should have returned a `frozenset` / `dict` upstream |
26
- | M4 | `extend(...)` into list consumers (acceptable) | Consumer's only operation is `accumulator.extend(producer())` and the accumulator is itself a list — cardinality is preserved by design, no dedup needed |
27
- | M5 | "Duplicates preserved" docstring (acceptable) | Producer's docstring explicitly states duplicates are part of the contract (e.g., for replay logs, audit trails, ordered streams) — no dedup required |
28
- | M6 | Producer signature widening | `Sequence[X]` widened to `Iterable[X]` (or `list[X]` → `Sequence[X]`) without re-validating each consumer's cardinality assumption |
29
- | M7 | Recursive / cycle-prone walkers | Walkers that traverse a graph or directory tree where a node can be re-entered via two paths MUST dedup at the walker boundary, not at every consumer |
30
- | M8 | Stream-fold accumulators | Generators / `yield`-based producers consumed by `list(...)` or `collections.Counter` — verify the consumer's cardinality expectation matches the producer's emission frequency |
31
-
32
- Customize per-artifact: a pure-function producer that returns a single value reduces to "verified clean — no collection involved"; a subprocess parser without a downstream consumer in the same PR may still need M1 satisfied by docstring text.
33
-
34
- ---
35
-
36
- ## Sample prompt
37
-
38
- The reusable Variant C template for Category M is in [`../prompts/category-m-producer-consumer-cardinality.md`](../prompts/category-m-producer-consumer-cardinality.md). Inline both the producer function and every consumer call site under `## Source material` so the audit can verify each cardinality boundary.
39
-
40
- ## Why Category M matters as its own bucket
41
-
42
- Categories A–K each examine one side of an interface in isolation. Category M examines the cardinality contract spanning two sides: the producer's "can my return value contain duplicates?" question and the consumer's "do I tolerate duplicates?" answer. A reviewer walking only A–K reads the producer, finds it correct on its own terms, and approves it — then reads the consumer separately and finds it also correct on its own terms. The bug emerges only when the two are exercised against the same input together.
43
-
44
- The pa#143 F10 case is the canonical worked example: `_extract_paths_from_everything_cli_stdout` returned `list[Path]` by walking the `es.exe` stdout line-by-line. The consumer iterated the list directly and ran one `INSERT INTO watched_dirs(path) VALUES (...)` per element against a table with a `UNIQUE(path)` constraint. When the subprocess emitted the same path on two lines (an edge case — Everything's stdout can repeat results across drive letters), the list carried the path twice, so the writeback ran the same `INSERT` twice; the second `INSERT` raised `sqlite3.IntegrityError: UNIQUE constraint failed: watched_dirs.path` and the entire watchdog crashed. The fix was to change the producer to `frozenset[Path]`, which deduplicates at the boundary so each path reaches the writeback once; the consumer's per-element `INSERT` loop was already correct. The contract was the bug.
@@ -1,54 +0,0 @@
1
- # Category N — Test-name scenario verifier
2
-
3
- **What this category audits:** tests whose names assert a specific scenario (`test_*_when_*`, `test_*_at_*`, `test_*_under_*`, `test_*_with_*`) must demonstrate via fixture inspection, monkeypatch state, or environment setup that the named condition is in effect when the system under test runs. Common failure: a test named `test_resolves_under_unc_prefix` constructs a regular drive-letter path, asserts the function returns a non-empty result, and passes — but the UNC-prefix branch was never exercised. The test is a load-bearing witness that the named scenario works, but it only exercises the no-op default path.
4
-
5
- **Why this category is its own bucket:** Categories A–K catch defects in the production code. Category N catches defects in the *test corpus's claim to coverage*. A green test suite is only meaningful when each test's name accurately describes what was exercised. When a test passes "for the wrong reason" — e.g., the assertion matches a substring that appears in both the scenario-named branch AND the default branch — the suite reports coverage of a scenario it never ran.
6
-
7
- **Examples of Category N findings:**
8
- - Cross-platform tests named `test_*_on_windows` / `test_*_on_linux` that don't gate on `sys.platform` or `monkeypatch.setattr(os, "name", ...)` and run unchanged on every platform. (ccc#476 F5, F21, F23, F26, F27)
9
- - String-shape tests that exercise only the no-op branch (`assert result == ""` after constructing an input that hits the early-return path, not the named scenario). (pa#135 F11, F15)
10
- - Integration tests with assertions like `<substring> not in executed_sql` where the substring shape never matches the SQL fragment shape — the assertion cannot fail by construction. (pa#136 F50)
11
- - Path-decision tests for `is_test_file` / `is_hook_infrastructure` / `_resolve_*_path` without a parametric matrix of canonical edge cases (empty string, tilde-prefix, UNC, drive-letter, symlinked, `..`-containing, trailing slash).
12
- - A test resolves `Path(__file__).parents[3]` expecting the `claude-dev-env/` package root, but the parents chain stops at `skills/` — the test cannot fail for the right reason and the asserted directory wiring is broken by construction.
13
- - A test imports the same function twice under two names (`from path_utils import is_config_file` plus `from path_utils import is_config_file as path_utils_is_config_file`) and asserts the two bound names produce the same result — the assertion cannot fail because both names are the same function object; the appearance of two parallel implementations is fake.
14
-
15
- **Companion reference:** see `../source-material-section-types.md`.
16
-
17
- ---
18
-
19
- ## Sub-bucket decomposition (Category N)
20
-
21
- Decomposition is by the **kind of scenario claim** the test name makes vs the evidence in its body.
22
-
23
- | ID | Axis name | Concrete checks |
24
- |---|---|---|
25
- | N1 | Scenario-named tests demonstrate the scenario | A test whose name contains `_when_`, `_at_`, `_under_`, `_with_`, `_on_`, `_after_`, `_during_` proves via monkeypatch / fixture / environment setup that the named condition holds during the act phase |
26
- | N2 | Path-decision parametric matrices | Tests for `is_*_path` / `_resolve_*_path` / `*_path_exemptions` modules ship a parametrized matrix covering: empty string, single filename, tilde-prefix, UNC path, drive-letter path, symlinked path, `..`-containing path, trailing-slash path |
27
- | N3 | Tests that pass "for the wrong reason" | The assertion's substring matches both the scenario-named branch and the default branch — the test cannot distinguish which branch executed |
28
- | N4 | No-op branch exercised by scenario name | The constructed input hits an early-return / guard / no-feature-installed branch BEFORE the scenario-named code runs; the assertion succeeds against the no-op output |
29
- | N5 | Assertion shape mismatch | The assertion shape (`<substring> not in result`, `result == ""`, `len(result) > 0`) cannot fail by construction against the return type / value space |
30
- | N6 | Cross-platform scenario gating | Tests named `_on_<platform>` MUST gate on `sys.platform`, `monkeypatch.setattr(os, ...)`, or `@pytest.mark.skipif(sys.platform != "<platform>")` — bare scenario names that run unchanged across platforms claim more than they prove |
31
- | N7 | Time / clock scenario gating | Tests named `_after_<duration>` / `_at_midnight` / `_during_business_hours` MUST inject a frozen clock (`freezegun.freeze_time`, `monkeypatch.setattr(time, "time", ...)`) — wall-clock tests are non-deterministic and may pass against the wrong scenario |
32
- | N8 | Concurrent / load scenario gating | Tests named `_under_load` / `_with_concurrent_writers` MUST spawn the concurrent workers and `wait()` on them — single-threaded tests cannot claim concurrent-scenario coverage |
33
- | N9 | Neutral-named tests (out of scope) | Tests named `test_returns_empty_list_for_unknown_key` / `test_handles_y` (no scenario claim in the name) are NOT subject to N1–N8; flag them only for assertion-shape mismatches under N5 |
34
- | N10 | Test fixture wiring correctness | The test's fixture / path / import wiring resolves to the artifact the test name claims. Path arithmetic (`Path(__file__).parents[k]`) reaches the directory the assertion expects — verify by walking the parents chain symbolically. Same-symbol dual imports (`from m import f` plus `from m import f as f_alias`) bind two names to the same function object, so any parity assertion between them is true by construction. Fixture file lookups (`open(Path(__file__).parent / 'fixture.txt')`) reach a file that exists. |
35
-
36
- Customize per-artifact: a pure-function test corpus with no scenario claims reduces N1–N4 to "verified clean — no scenario-named tests in scope"; a path-classifier PR may need N2 exhausted across 8+ canonical inputs.
37
-
38
- ---
39
-
40
- ## Write-time advisory for the flag-gated N1 slice
41
-
42
- `check_flag_gated_scenario_test_naming` in `code_rules_test_assertions.py` (wired into `code_rules_enforcer.py`) catches the deterministic N1 slice at Write/Edit time. When two or more sibling tests in a file `monkeypatch.setattr` the same module-level UPPER_SNAKE flag, that flag governs which branch the code under test runs. A `test_*` whose name carries a scenario clause (`_when_`, `_passes`, `_succeeds`, `_on_clean`) but never patches that flag runs under the flag's default value, so its named condition may not be in effect. The check prints an advisory to stderr and never blocks the write — the breadth of the sibling-pattern heuristic suits a judgment lane, where the author decides whether to patch the flag (and assert the gated path runs) or rename the test. The audit still owns the full N1–N10 surface; the advisory only surfaces this one mechanically-detectable shape early.
43
-
44
- ---
45
-
46
- ## Sample prompt
47
-
48
- The reusable Variant C template for Category N is in [`../prompts/category-n-test-name-scenario-verifier.md`](../prompts/category-n-test-name-scenario-verifier.md). Inline every changed test function under `## Source material` along with the production function it claims to cover, so the audit can compare the named scenario against the body's act phase.
49
-
50
- ## Why Category N matters as its own bucket
51
-
52
- Categories A–K each examine production code. Category N examines whether the test corpus's name claims match its body evidence. A green suite without N coverage gives a false sense of confidence: scenario-named tests that pass against the no-op branch report success for code that was never run.
53
-
54
- The ccc#476 cases (F5, F21, F23, F26, F27) demonstrate the cost of not running N: tests named `_on_windows` and `_on_linux` ran unchanged on every CI runner. The Windows-specific test passed on Linux because the production function's Windows branch was guarded by `sys.platform == "win32"` and short-circuited — the assertion succeeded against the Linux fallback. The test name claimed Windows coverage; the body never exercised it. Only an N6-style gating audit (require `monkeypatch.setattr(sys, "platform", "win32")` inside any `_on_windows`-named test, or `@pytest.mark.skipif`) would have caught this in review.
@@ -1,59 +0,0 @@
1
- # Category P — Name / regex / word-list vs behavior-contract precision
2
-
3
- **What this category audits:** identifiers and reference data whose label asserts a contract the body does not deliver. The label may be too broad (`is_inside_function` flag set on def but never reset on scope exit), too narrow (`_is_docstring_section_header` matching only terminating headers), or shaped as one thing while behaving as another (`FILE_PATH_PATTERN` regex matching `client/server` because it lacks path-shape anchors; a hard-deny replacement word list containing ordinary technical English).
4
-
5
- The label-vs-body gap is its own failure mode independent of behavior-equivalence (L) because nothing was rewritten — the contract is broken at the moment the name is first chosen. The hook-enforced naming rules (J5 abbreviations, J6 vague nouns) ban specific identifiers but say nothing about precision-of-fit between what a name promises and what its body does.
6
-
7
- **Examples of Category P findings:**
8
- - A flag `is_inside_function` set on `def` and never reset on scope exit — name asserts state the body fails to keep.
9
- - A helper `_split_module_stem_prefix` returning `code_rules` which substring-matches unrelated stems (`code_ruleset.py`) — name asserts the tighter contract; body delivers a looser one.
10
- - A predicate `_is_docstring_section_header` matching only terminating headers — name asserts the general case; body delivers a specific subset.
11
- - A regex `FILE_PATH_PATTERN = r"(\S+/\S+)"` unanchored — name asserts path-shape; body accepts any `word/word`.
12
- - A hard-deny replacement-by-term list including `command`, `address`, `function`, `subject`, `however`, `forward` — list name asserts "banned heavy words"; entries are ordinary technical vocabulary.
13
-
14
- **Companion reference:** see `../source-material-section-types.md`.
15
-
16
- ---
17
-
18
- ## Binding counterexample protocol
19
-
20
- For every regex, brace or token scanner, or word-list the diff adds or edits, build at least three concrete inputs and trace each one through the pattern by hand before you call the pattern clean:
21
-
22
- - **One intended match** — an input the pattern must catch.
23
- - **One near-miss** — an input a quick reading would expect to catch, but the contract says the pattern must reject.
24
- - **One structural edge** — an input whose shape stresses the scanner: a destructured parameter, a quoted key, a nested brace, an escaped delimiter.
25
-
26
- Walk each input character by character (or token by token) and write down what the pattern returns. A pattern that returns the wrong answer on any of the three inputs is a Category P finding; cite the input that breaks it. The pattern is clean only after all three inputs return the answer the contract promises.
27
-
28
- `hooks/blocking/` is the priority surface. Its patterns gate every write, so a pattern that matches too much or too little there fires on edits across the whole codebase.
29
-
30
- Two canonical failures show why the hand trace is binding:
31
-
32
- - **Brace scan that reads a parameter as body.** A scanner counts braces from the signature's opening `(` and treats the first balanced close as the end of the function body. A destructured parameter closes a brace inside the signature — `renderRow({ id, label })` in JavaScript, or a `{a, b}` binding in another language — so the scanner marks the body one token too early and reads the parameter list as body code.
33
- - **`\bschema\b` that matches a value reference.** A `\bschema\b` pattern targets a `schema` declaration, yet it also matches `schema` as the value in `{ label: schema }`, where the word is a reference, not the declaration the pattern means to find. A word boundary alone does not tell a declaration from a key or a value.
34
-
35
- ---
36
-
37
- ## Sub-bucket decomposition (Category P)
38
-
39
- Decomposition is by the **kind of identifier / reference data** whose label is being audited against its body.
40
-
41
- | ID | Axis name | Concrete checks |
42
- |---|---|---|
43
- | P1 | Boolean / flag names assert state the body keeps | A `is_*` / `has_*` / `was_*` / `should_*` flag's lifecycle in the body matches what the name promises: set when the named condition becomes true, reset when it becomes false. Flags set once and never reset are P1 findings. |
44
- | P2 | Predicate-name breadth matches body coverage | A `_is_*` / `_has_*` predicate function — the body covers exactly the input class the name names. Bodies matching a narrower subset ("section header" name matching only terminating section headers) or a broader superset ("shared temp resolution" name matching shared temp AND HOME/TMP env-derived paths) are P2 findings. |
45
- | P3 | Regex name vs regex shape | A `*_PATTERN` / `*_REGEX` constant — the regex includes the anchors (^, $, \b, lookarounds) the name implies. An unanchored regex named `FILE_PATH_PATTERN` matching `word/word` is a P3 finding. |
46
- | P4 | Helper-function name vs return contract | A helper-function name (`_split_module_stem_prefix`, `_resolve_*`, `_extract_*`) — the return shape and matching semantics deliver what the name promises. Helpers whose return value's matching surface is looser than the name suggests (a stem-prefix substring-matching unrelated stems) are P4 findings. |
47
- | P5 | Word-list / replacement-table precision | A reference list named for a specific class of inputs (`HARD_DENY_REPLACEMENT_TERMS`, `BANNED_PROMPT_PHRASES`, `VAGUE_ADJECTIVES`) — every entry must satisfy the named class. Entries that are common in legitimate inputs (`command`, `function`, `however` in a list named "heavy words to ban") are P5 findings. |
48
- | P6 | Class / module name vs scope | A class name (`SingleFileParser`) or module name (`enforcer.py`) — the body's responsibility fits the name's named scope. A class that grew responsibilities outside its name is a P6 finding. |
49
- | P7 | Reverse: name understates what the body does | A name that promises a narrow contract while the body delivers a broader effect — future callers may rely on the narrow contract and be surprised. (Symmetric mirror of P2 / P3.) |
50
-
51
- ---
52
-
53
- ## Sample prompt
54
-
55
- The reusable Variant C template for Category P is in [`../prompts/category-p-name-vs-behavior-contract.md`](../prompts/category-p-name-vs-behavior-contract.md). Inline every newly-added or renamed identifier alongside the body code that implements its contract under `## Source material`.
56
-
57
- ## Why Category P matters as its own bucket
58
-
59
- Category L (behavior-equivalence) audits a rewrite against a prior implementation — it only fires when there is a `before` state to compare. Category P audits a fresh identifier whose label asserts a contract; the bug is that the body never delivered the named contract, even on the first commit. The hook-enforced J5 / J6 naming rules ban specific identifiers (`ctx`, `cfg`, `data`, `result`, `handle_*`) but say nothing about whether the identifier the author chose matches the body's reach. P is the bucket that catches a regex named `FILE_PATH_PATTERN` that accepts `TCP/IP`, a hard-deny word list that bans `function` and `address`, and a predicate named for the general case that only handles a subset — at audit time, before the gate ships and starts producing false positives in production.
@@ -1,46 +0,0 @@
1
- # Category Q — Cross-surface claim consistency (terminology, PR-description claims, message-vs-guard)
2
-
3
- **What this category audits:** claims a change makes on one surface that a second surface contradicts. A code field carries one spelling while the prose that names it uses a near-miss variant. A PR description states a fact that the diff or the repo does not bear out. A log or error string asserts a condition that the branch gating it does not guarantee. Each defect lives in the gap between two surfaces that must agree: the code and the prose about it, the PR body and the diff, the message text and the guard that fires it.
4
-
5
- **Why this category is its own bucket:** each surface reads as correct on its own. The code field is a valid identifier. The PR sentence is a well-formed claim. The log line is grammatical. The defect surfaces only when a reader holds two surfaces side by side and finds that one says a thing the other denies. Categories that read a single surface miss this; Q forces the reader to pair every claim with the surface that must back it.
6
-
7
- ## Canonical examples
8
-
9
- - **PR #810 (Q1).** Eleven prose surfaces name the term one way while the code field reads `premium_interactions`. One divergent term spreads across README text, doc tables, and inline prose, so a reader who trusts any of the eleven surfaces reaches for a field name the code does not define.
10
- - **PR #808 (Q2).** The PR description claims zero remaining gate subjects while `converge.mjs` still holds them. The body asserts a cleanup the diff does not finish, so a reviewer who trusts the description approves a state the code contradicts.
11
- - **PR #823 (Q3).** A skip-branch log asserts a hardening PR opened, yet `spawnStandardsFollowUp` can return `hardeningPrOpened: false`. The message states an outcome the guard that reaches it does not guarantee, so the log reads as a success the run did not achieve.
12
-
13
- ## Other typical patterns
14
-
15
- - A config key the diff adds reads `retry_budget`; a doc table row names it `retry-budget`, and a runbook names it `retry budget`.
16
- - An API field the diff introduces is `itemCount` (singular tail); a client doc calls it `itemCounts`.
17
- - A PR body says "this removes the last caller of `legacy_resolve`"; a reference search still finds a caller in a sibling module.
18
- - A PR body claims a 30-second ceiling; the config value the diff sets reads 60 seconds.
19
- - An error string says "config file is missing" while the branch that raises it fires on a parse failure, not an absent file.
20
- - A log line says "retry limit reached this run" while the counter it reads resets each round, so the wording overstates the lifetime of the state it names.
21
-
22
- **Companion reference:** see `../source-material-section-types.md`.
23
-
24
- ---
25
-
26
- ## Sub-bucket decomposition (Category Q)
27
-
28
- Decomposition is by the **pair of surfaces** that must agree.
29
-
30
- | ID | Axis name | Concrete checks |
31
- |---|---|---|
32
- | Q1 | Term sweep | Pull out every identifier, API field, and config key the diff introduces or reads. Sweep all changed prose surfaces for near-miss variants — hyphen or space in place of an underscore, singular where the code is plural (or the reverse), a shared prefix with a divergent tail. Run `packages/claude-dev-env/_shared/pr-loop/scripts/terminology_sweep.py` for mechanical recall — it reads a unified diff on stdin or via `--diff-file`, prints one `file:line` finding per near-miss, exits 1 when findings exist and 0 when clean — then adjudicate each finding, since a near-miss can be an intentional distinct term. |
33
- | Q2 | PR-description claim verification | Fetch the PR body. For each falsifiable claim, verify it two ways against the diff and the repo. A dead-code claim ("removes the last caller of X") gets a reference search across the repo. A performance or ceiling claim ("caps at 30 seconds", "cuts calls in half") gets checked against the config values and call sites the diff sets. |
34
- | Q3 | Message-vs-guard consistency | For every log, error, and evidence string the diff adds or edits, read the condition that gates it. The message text matches what the guard guarantees — a string that asserts an outcome fires only on a branch that reaches that outcome. Temporal wording matches the lifetime of the state variable it names: "this run" versus "this round", "is open" versus "was opened", "reached" versus "attempted". |
35
-
36
- Customize per-artifact: a change that adds no new identifier and no PR-body claim reduces Q to "verify every message the diff touches still matches its guard." A rename that spreads a term across many docs may need Q1 alone to list every prose surface.
37
-
38
- ---
39
-
40
- ## Sample prompt
41
-
42
- The reusable template for Category Q is in [`../prompts/category-q-cross-surface-claims.md`](../prompts/category-q-cross-surface-claims.md). The Category Q source-material block needs the diff, the PR body text, and the changed prose surfaces the agent must cross-reference against the code.
43
-
44
- ## Why Category Q matters as its own bucket
45
-
46
- A reviewer walking a single surface at a time — the code alone, the docs alone, the PR body alone — judges each on its own merits and misses Q. The code field is a valid name; the PR sentence is a plausible claim; the log line is grammatical. Only pairing each claim with the surface that must back it surfaces the gap. Q forces that pairing: every identifier the diff touches is swept against the prose, every PR-body claim is checked against the diff and the repo, and every message is read against the guard that fires it.
@@ -1,407 +0,0 @@
1
- Audit [REPO/ARTIFACT] [TARGET_ID] for **Category A only** (API contract verification). Skip B–P. Sub-bucket forced-exhaustion mode: Category A is decomposed into 9 sub-buckets below. Each sub-bucket REQUIRES at least one Shape A finding OR exactly one Shape B proof-of-absence with **at least 3 adversarial probes** specific to that sub-bucket. A sub-bucket returning neither is a protocol gap.
2
-
3
- [ARTIFACT METADATA: title / change description / head SHA or revision identifier / scope summary]
4
- ID prefix: `find`.
5
-
6
- ## Source material ([N] files/sections, all lines in scope)
7
-
8
- [INLINE THE FULL ARTIFACT HERE — see ../source-material-section-types.md for chunking guidance.]
9
-
10
- ## Sub-buckets (each requires Shape A finding OR Shape B with ≥3 adversarial probes)
11
-
12
- **A1. Function/method signatures vs internal call sites**
13
- - Enumerate every defined function or method's parameter list: count, names, defaults, kw-only barriers (Python `*`), positional-only barriers (`/`), variadic markers (`*args`, `**kwargs`).
14
- - For every internal call within the artifact, verify the binding matches the callee's signature: positional count, keyword names, required-vs-optional, default fall-through.
15
- - Flag positional arguments passed to keyword-only parameters and vice versa.
16
- - Flag calls that omit a required parameter relying on a default that does not exist on the current branch.
17
- - Verify decorators (`@staticmethod`, `@classmethod`, `@property`) do not silently shift the parameter binding (e.g., `self` / `cls` insertion).
18
- - Confirm sync-vs-async (is the symbol `async def`?), the exact access path a caller uses (free function vs instance method via an object attribute vs import path), and that a keyword-only parameter with no default is required — omitting it raises `TypeError`.
19
- - When a call passes a config value as an argument, confirm the value's documented role matches the parameter's role (poll interval vs timeout vs budget). A poll interval such as `PollingIntervals.resume_check` handed to a `progress_check_timeout` parameter type-checks but carries the wrong meaning.
20
-
21
- **A2. Return-type annotation vs every code path**
22
- - For each annotated function, walk every code path: explicit `return X`, fall-through to implicit `None`, exception-handler exit, generator `yield` paths, async coroutine return value.
23
- - Verify each path's return value is assignable to the declared annotation; flag `-> bool` functions that can return `None`, `-> list[T]` functions that can return `None` on an early exit, etc.
24
- - For functions that raise instead of returning on some path, confirm the annotation does not promise a value the caller will dereference.
25
- - Inspect `try/except/finally` chains for paths that return from `finally` and override `try`/`except` returns.
26
- - For async functions, confirm the annotation refers to the awaited type, not the coroutine wrapper.
27
- - The full failure contract is the return value AND every exception raised — list each `raise` in the body and the docstring `Raises:`; a `-> bool` that can also raise is not fully described as "returns bool".
28
-
29
- **A3. CLI/argument-parser declaration → downstream Namespace contract**
30
- - For every `add_argument(...)` (or equivalent CLI declaration), verify the auto-derived or explicit `dest=` matches the attribute name accessed downstream on the parsed namespace.
31
- - Verify `type=` (or schema coercion) matches every downstream consumer's expectation — e.g., a value handed to a function requiring `int` is declared `type=int`, not the default `str`.
32
- - Switch flags (`action="store_true"` / `store_false`) produce booleans; non-switch arguments produce typed values; flag the mismatch where a switch is treated as a value or vice versa.
33
- - Default values resolve correctly when the flag is omitted; flag any code path that assumes the user supplied the argument.
34
- - Required-vs-optional declaration matches the downstream code's null-handling.
35
-
36
- **A4. Stdlib/library callback contracts**
37
- - Identify every callback handed to a library function (e.g., `os.walk(onerror=...)`, sort `key=`, `filter`, `map`, `re.sub(repl=callable)`, signal handlers, threading callbacks). Verify each callback's signature matches what the library calls it with — arity, positional-vs-keyword, return type the library consumes.
38
- - For every stdlib function the artifact calls, verify argument types and exception contracts: which exceptions can each call raise, and is each caller prepared (or deliberately not prepared) for them.
39
- - Verify kwargs to stdlib functions are spelled correctly for the targeted runtime version (deprecated/renamed kwargs, version-introduced kwargs).
40
- - Catch-site precision — for any "catches X" claim confirm the exact catch site and scope (an `except` around only a rollback inside `finally` does not catch the same error from the `with` body).
41
- - Flag callbacks whose return value the library consumes but the implementation returns `None` (or vice versa).
42
- - Confirm callback exception behavior: which exceptions in the callback bubble out, which are swallowed by the library, which terminate iteration.
43
-
44
- **A5. Subprocess / external-process invocation contract**
45
- - For every `subprocess.run` / `subprocess.Popen` / equivalent call, verify the `args` shape: list-of-strings vs single string vs `shell=True` semantics.
46
- - Verify kwargs are valid for the targeted runtime version (`capture_output`, `text`, `encoding`, `check`, `timeout`); flag combinations that conflict (`stdout=PIPE` + `capture_output=True`).
47
- - Exception contract under `check=True` (raises `CalledProcessError` on non-zero exit) — verify callers either propagate or handle, with no silent swallow that masks failure.
48
- - Verify quoting/escaping of arguments crossing the subprocess boundary, especially when interpolating untrusted strings.
49
- - Verify the resolved executable path exists on the target platform, not assumed (`which` / `Get-Command` failure paths).
50
-
51
- **A6. Shell/host-language cmdlet or function parameter sets and binding**
52
- - For every shell or host-language function/cmdlet declaration with parameter sets (PowerShell `param(...)` with `ParameterSetName=`, Bash `getopts`, etc.), verify the declaration matches every invocation pattern. Confirm a default parameter set is declared if no-arg invocation is reachable.
53
- - For every cmdlet/builtin invocation, verify the parameter combination is valid per the cmdlet's documented parameter sets — flag combinations that mix flags from disjoint parameter sets.
54
- - Flag missing `-ErrorAction` (or equivalent) declarations on calls whose null-checks downstream assume swallowed errors.
55
- - Verify each cmdlet/builtin argument's type coercion at the boundary matches what the cmdlet expects.
56
- - Confirm pipeline-bound parameters (`ValueFromPipeline`, `ValueFromPipelineByPropertyName`) match what upstream emits.
57
-
58
- **A7. Cross-language / cross-process argv and serialization boundary**
59
- - Trace every value crossing a language or process boundary (shell argv → Python `sys.argv`, environment variables, JSON/IPC payloads, file-format round-trips). Verify the producer's serialization matches the consumer's parser.
60
- - Flag trailing-backslash, embedded-space, embedded-quote, and Unicode hazards on Windows argv composition (Microsoft C-runtime argv parser rules) and POSIX shell word-splitting.
61
- - Verify argument-order conventions match across the boundary — e.g., flag order, positional placement, separator handling (`--`).
62
- - Cross-language default-value drift: a default declared on one side that differs from the default on the other side; verify either both are sourced from a single config or both are intentionally mirrored.
63
- - Cross-language type drift: integer width, signed/unsigned, floating-point precision, string encoding (UTF-8 vs UTF-16), null/empty-string semantics.
64
-
65
- **A8. Documented API/tool calls vs official API documentation**
66
- - For every API call, MCP tool invocation, CLI command, or SDK method call documented in the source material, identify the provider.
67
- - Look up the official documentation for that API (Context7 MCP for libraries/SDKs, API reference docs for REST endpoints, tool definitions in session for MCP calls, `--help` for CLI tools).
68
- - Verify the documented parameter names, types, and required-ness match the official API signature.
69
- - For read-only API calls, execute one safe invocation to confirm the documented shape succeeds in practice.
70
- - For write calls, verify the signature against the provider's own published API contract — their REST reference docs, OpenAPI spec, SDK source code, or `--help` output. When a read endpoint exposes the same state, call it to confirm the write contract.
71
- - Flag every call where documented parameters, types, or behavior diverge from the official API contract.
72
-
73
- **A9. Intra-module sibling-helper API parity**
74
- - Did the diff add a new check / validator / parser / handler alongside existing sibling helpers in the same module? Verify the new one matches the sibling cohort's signature — every parameter the peer checks accept (e.g., `all_changed_lines` for diff-line filtering).
75
- - Verify the new helper's scoping semantics match the cohort: whole-file vs fragment content surface, diff-line filtering, and `defer_scope_to_caller` handling.
76
- - Verify the new helper's result-shape contract matches: where the result cap is applied (pre-scope vs post-scope), whether `defer_scope_to_caller=True` is honored, and the return type.
77
- - When the new helper omits a sibling-accepted parameter, runs on a different content surface than its siblings, or applies the result cap at a different point in the pipeline, name it as an A9 finding. Cite the new helper and the sibling it diverges from as the pair.
78
- - For a pure-code artifact with no new sibling helper, A9 is one line of proof-of-absence (the diff adds no helper alongside an existing cohort).
79
-
80
- ### Documentation as contract (when the artifact asserts facts about the code)
81
-
82
- When the artifact is documentation that asserts facts about the codebase (symbol names, signatures, return types, exceptions, file paths), run all seven documentation-as-contract checks below; each yields a confirmation or a finding. For a pure-code artifact, this section is one line of proof-of-absence (the artifact asserts no code facts).
83
-
84
- - Full failure contract — the failure signals of a function are its return value AND every exception it raises; trace the body and the docstring `Raises:` for every `raise`. _Example:_ a docs PR says a UI helper "returns `bool`", but it also raises a custom not-found error, so "returns bool" understates the contract.
85
- - Call shape — required versus optional parameters (a keyword-only parameter with NO default is required; omitting it raises `TypeError`), sync versus async, and the exact access path (free function versus instance method reached through an object attribute versus import path). _Example:_ a doc presents a helper as a free function, but it is an `async` instance method reached through an object attribute, so the doc's call example would raise `TypeError`.
86
- - Reuse-first — before a doc endorses a hand-written snippet, search for a dedicated helper that already does it. _Example:_ a doc endorses hand-composing `normalize(name).lower()` inline while a dedicated `normalize_for_matching()` helper already does exactly that.
87
- - Path resolution — every file or directory path a doc cites resolves from the repository root. _Example:_ a doc cites a bare `snapshots/` directory as if it sat at the repo root, but the tree lives under `subsystem/snapshots/`.
88
- - Cross-entry consistency — scan parallel rows, sections, and table entries for claims that contradict each other. _Example:_ two adjacent table rows map the same subsystem to two different exception base classes.
89
- - Catch-site precision — when a doc claims code "catches X", confirm the exact site and scope of the catch. _Example:_ a doc says a context manager catches a driver error, but the `except` wraps only the rollback inside `finally`, so an error raised in the `with` body propagates uncaught.
90
- - Citation freshness — re-derive every `file:line` claim against the current code; never trust a prior "verified" assertion or wording borrowed from a comment. _Example:_ an attribute name carried over from a review comment names a member the class does not define; the current code exposes it under a different name.
91
-
92
- ## Cross-bucket questions to answer at the end
93
-
94
- Q1: Are there any contracts that span two sub-buckets that single-bucket analysis would miss?
95
- Q2: What is the worst contract-drift hazard introduced by this artifact? Cite file:line.
96
- Q3: Where would a future refactor most likely break a cross-bucket or cross-language contract? Name the line(s) most fragile.
97
-
98
- ## Output
99
-
100
- Lead: `Total: N (P0=N, P1=N, P2=N)`. For each sub-bucket A1–A9, produce Shape A or Shape B (with ≥3 adversarial probes). Documentation-as-contract: when the artifact asserts code facts, walk all seven checks and report each as a finding or a confirmation; for a pure-code artifact, one line of proof-of-absence. Cross-bucket Q1–Q3 answers after the per-sub-bucket walk. Adversarial second pass: "assume your first pass missed at least 3 P1 bugs across these 9 sub-buckets — find them." Open Questions section for ambiguities. Read-only. No edits, no commits.
101
-
102
- ---
103
-
104
- # Worked example: jl-cmd/claude-dev-env PR #394 (May 2026 audit experiment)
105
-
106
- Audit jl-cmd/claude-dev-env PR #394 for **Category A only** (API contract verification). Skip B–N. Sub-bucket forced-exhaustion mode: Category A is decomposed into 8 sub-buckets below. Each sub-bucket REQUIRES at least one Shape A finding OR exactly one Shape B proof-of-absence with **at least 3 adversarial probes** specific to that sub-bucket. A sub-bucket returning neither is a protocol gap.
107
-
108
- PR: feat(scripts): add sweep-empty-dirs utility and scheduled-task installer
109
- Head SHA: 62c9c169ee7a44824e5da25c4cf8b74fdca08a53
110
- ID prefix: `find`.
111
-
112
- ## Sub-buckets (each requires Shape A finding OR Shape B with ≥3 adversarial probes)
113
-
114
- **A1. Python function signatures vs internal call sites**
115
- - Every defined function's parameter list (count, names, defaults, kw-only).
116
- - Every internal call within `sweep_empty_dirs.py` matches its target's signature.
117
-
118
- **A2. Python return-type annotation vs every code path**
119
- - Each function's return annotation is satisfied by every path: explicit `return X`, fall-through, exception-handler exit.
120
- - Pay specific attention to `main() -> None` (with the `KeyboardInterrupt` path) and `sweep(...) -> list[str]` (single return path).
121
-
122
- **A3. argparse parser → Namespace contract**
123
- - Every `add_argument(...)` declared in `_build_parser()` produces the exact dest name accessed in `main()`.
124
- - `type=` matches downstream usage (e.g., `time.sleep` accepts the type).
125
- - Switch flag (`--once`) produces a bool; non-switches produce typed values.
126
- - Default values resolve correctly when the flag is omitted.
127
-
128
- **A4. stdlib callback contracts (os.walk onerror, getctime, rmdir)**
129
- - `os.walk(root, onerror=_log_walk_error, topdown=False)` — `_log_walk_error` matches the exact signature stdlib calls (positional `OSError`).
130
- - `os.path.getctime`, `os.rmdir` argument and exception contracts.
131
- - `time.sleep` argument contract.
132
- - `os.walk`'s own kwargs (`onerror`, `topdown`) are spelled correctly for the Python version targeted.
133
-
134
- **A5. subprocess invocation contract (test file)**
135
- - `subprocess.run([list], check=True, capture_output=True)` — kwargs valid for the targeted Python.
136
- - The list shape — `["powershell", "-Command", string]` — matches `subprocess.run`'s expected `args` parameter.
137
- - Exception contract: `check=True` raises CalledProcessError on non-zero exit; tests do not catch it.
138
-
139
- **A6. PowerShell cmdlet parameter sets and binding**
140
- - `param(...)` declarations with `ParameterSetName=` — does the script declare `[CmdletBinding(DefaultParameterSetName=...)]`? If not, what happens with no-arg invocation?
141
- - `New-ScheduledTaskTrigger -Daily -At ... -RepetitionInterval ...` — is `-RepetitionInterval` valid for the `-Daily` parameter set per Microsoft's docs? Verify against https://learn.microsoft.com/powershell/module/scheduledtasks/new-scheduledtasktrigger.
142
- - `Get-Command python` (line 80) — missing `-ErrorAction SilentlyContinue` in the fallback breaks the subsequent null-check contract.
143
- - `Register-ScheduledTask`, `New-ScheduledTaskAction`, `New-ScheduledTaskSettingsSet`, `Get-ScheduledTask`, `Unregister-ScheduledTask` — verify each call's parameter shape against its cmdlet signature.
144
-
145
- **A7. Cross-language argv boundary (PowerShell argv construction → Python sys.argv → argparse)**
146
- - The `New-ScheduledTaskAction -Argument` string `"$ScriptPath --once --age $AgeSeconds ""$Target"""` — when expanded and passed to Windows process creation, does the resulting argv match what argparse expects?
147
- - Trailing-backslash hazard: if `$Target` ends with `\`, what does the Microsoft C-runtime argv parser produce?
148
- - The Python `argparse` flag order — does the launcher's argv arrangement (`<flags> <positional>`) match what argparse accepts?
149
- - Cross-language default-value drift: PowerShell `[int]$AgeSeconds = 120` vs Python `DEFAULT_AGE_SECONDS: int = 120` — both hardcoded, no shared source of truth.
150
- - Cross-language type drift: PowerShell `[int]` vs Python `type=int` — both convert to integer, but PowerShell's `[int]` is 32-bit signed; argparse's `int(...)` is Python int (arbitrary precision). Match for normal values.
151
-
152
- ## Cross-bucket questions to answer at the end
153
-
154
- Q1: Are there any contracts that span two sub-buckets that single-bucket analysis would miss?
155
- Q2: What's the worst contract-drift hazard introduced by this PR? Cite file:line.
156
- Q3: Where would a future refactor most likely break a cross-language contract? Name the line(s) most fragile.
157
-
158
- ## Output
159
-
160
- Lead: `Total: N (P0=N, P1=N, P2=N)`. For each sub-bucket A1-A7, produce Shape A or Shape B (with ≥3 probes). Cross-bucket Q1-Q3 answers after the per-sub-bucket walk. Adversarial second pass: "assume your first pass missed at least 3 P1 bugs across these 8 sub-buckets — find them." Open Questions section for ambiguities. Read-only. No edits, no commits.
161
-
162
- ## Diff (4 new files, all lines in scope)
163
-
164
- ### packages/claude-dev-env/scripts/sweep_empty_dirs.py
165
- ```python
166
- #!/usr/bin/env python3
167
- """Delete empty directories older than 2 minutes under a given root."""
168
-
169
- import argparse
170
- import os
171
- import sys
172
- import time
173
-
174
- from config.sweep_config import DEFAULT_AGE_SECONDS
175
- from config.sweep_config import DEFAULT_POLL_INTERVAL
176
-
177
-
178
- def _log_walk_error(os_error: OSError) -> None:
179
- print(f"warning: cannot scan {os_error.filename} — {os_error.strerror}", file=sys.stderr)
180
-
181
-
182
- def sweep(root: str, min_age_seconds: int) -> list[str]:
183
- """Remove empty directories under *root* older than *min_age_seconds*."""
184
-
185
- now = time.time()
186
- removed: list[str] = []
187
-
188
- for each_directory_path, _, _ in os.walk(
189
- root, onerror=_log_walk_error, topdown=False
190
- ):
191
- try:
192
- created = os.path.getctime(each_directory_path)
193
- except OSError:
194
- continue
195
- if now - created >= min_age_seconds:
196
- try:
197
- os.rmdir(each_directory_path)
198
- print(f"deleted: {each_directory_path}")
199
- removed.append(each_directory_path)
200
- except OSError:
201
- pass
202
-
203
- return removed
204
-
205
-
206
- def _build_parser() -> argparse.ArgumentParser:
207
- parser = argparse.ArgumentParser(description="Delete empty directories older than a given age.")
208
- parser.add_argument("root", help="Root directory to scan")
209
- parser.add_argument("--age", type=int, default=DEFAULT_AGE_SECONDS,
210
- help=f"Minimum age in seconds (default: {DEFAULT_AGE_SECONDS} = 2 minutes)")
211
- parser.add_argument("--once", action="store_true",
212
- help="Single pass and exit instead of watching in a loop")
213
- parser.add_argument("--interval", type=int, default=DEFAULT_POLL_INTERVAL,
214
- help=f"Poll interval in seconds when looping (default: {DEFAULT_POLL_INTERVAL})")
215
- return parser
216
-
217
-
218
- def main() -> None:
219
- parser = _build_parser()
220
- arguments = parser.parse_args()
221
-
222
- if not os.path.isdir(arguments.root):
223
- print(f"error: not a directory: {arguments.root}", file=sys.stderr)
224
- sys.exit(1)
225
-
226
- if arguments.once:
227
- sweep(arguments.root, arguments.age)
228
- return
229
-
230
- print(f"watching {arguments.root} every {arguments.interval}s (age threshold: {arguments.age}s)")
231
- try:
232
- while True:
233
- sweep(arguments.root, arguments.age)
234
- time.sleep(arguments.interval)
235
- except KeyboardInterrupt:
236
- print("\nstopped.")
237
-
238
-
239
- if __name__ == "__main__":
240
- main()
241
- ```
242
-
243
- ### packages/claude-dev-env/scripts/config/sweep_config.py
244
- ```python
245
- """Centralized timing configuration for sweep_empty_dirs."""
246
-
247
- DEFAULT_AGE_SECONDS: int = 120
248
- DEFAULT_POLL_INTERVAL: int = 30
249
- ```
250
-
251
- ### packages/claude-dev-env/scripts/tests/test_sweep_empty_dirs.py
252
- ```python
253
- """Tests for sweep_empty_dirs.py"""
254
-
255
- from __future__ import annotations
256
-
257
- import datetime
258
- import os
259
- import subprocess
260
- import sys
261
- import tempfile
262
- import time
263
- from pathlib import Path
264
-
265
- _SCRIPTS_DIR = Path(__file__).resolve().parent.parent
266
- if str(_SCRIPTS_DIR) not in sys.path:
267
- sys.path.insert(0, str(_SCRIPTS_DIR))
268
-
269
- from sweep_empty_dirs import sweep # noqa: E402
270
-
271
-
272
- def _set_creation_time_windows(path: str, timestamp: float) -> None:
273
- dt = datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)
274
- date_str = dt.strftime("%Y-%m-%d %H:%M:%S")
275
- subprocess.run(
276
- ["powershell", "-Command",
277
- f"(Get-Item '{path}').CreationTimeUtc = [DateTime]'{date_str}'"],
278
- check=True, capture_output=True,
279
- )
280
-
281
-
282
- def test_deletes_empty_dir_older_than_threshold() -> None:
283
- with tempfile.TemporaryDirectory() as tmp:
284
- empty_dir = os.path.join(tmp, "old_empty")
285
- os.mkdir(empty_dir)
286
- _set_creation_time_windows(empty_dir, time.time() - 300)
287
- removed = sweep(tmp, min_age_seconds=120)
288
- assert empty_dir in removed
289
- assert not os.path.isdir(empty_dir)
290
-
291
-
292
- def test_skips_empty_dir_newer_than_threshold() -> None:
293
- with tempfile.TemporaryDirectory() as tmp:
294
- fresh_dir = os.path.join(tmp, "fresh_empty")
295
- os.mkdir(fresh_dir)
296
- removed = sweep(tmp, min_age_seconds=120)
297
- assert fresh_dir not in removed
298
- assert os.path.isdir(fresh_dir)
299
-
300
-
301
- def test_deletes_nested_empty_dirs() -> None:
302
- with tempfile.TemporaryDirectory() as tmp:
303
- leaf = os.path.join(tmp, "parent", "child", "leaf")
304
- os.makedirs(leaf)
305
- _set_creation_time_windows(os.path.join(tmp, "parent"), time.time() - 300)
306
- _set_creation_time_windows(os.path.join(tmp, "parent", "child"), time.time() - 300)
307
- _set_creation_time_windows(leaf, time.time() - 300)
308
- removed = sweep(tmp, min_age_seconds=120)
309
- assert leaf in removed
310
- assert os.path.join(tmp, "parent", "child") in removed
311
- assert os.path.join(tmp, "parent") in removed
312
-
313
-
314
- def test_empty_root_does_not_crash() -> None:
315
- with tempfile.TemporaryDirectory() as tmp:
316
- _set_creation_time_windows(tmp, time.time() - 300)
317
- sweep(tmp, min_age_seconds=120)
318
-
319
-
320
- def test_skips_nonempty_dir() -> None:
321
- with tempfile.TemporaryDirectory() as tmp:
322
- nonempty_dir = os.path.join(tmp, "has_stuff")
323
- os.mkdir(nonempty_dir)
324
- Path(nonempty_dir, "keepme.txt").write_text("hello")
325
- removed = sweep(tmp, min_age_seconds=0)
326
- assert nonempty_dir not in removed
327
- assert os.path.isdir(nonempty_dir)
328
- ```
329
-
330
- ### packages/claude-dev-env/scripts/Install-SweepEmptyDirs.ps1
331
- ```powershell
332
- #!/usr/bin/env pwsh
333
- param(
334
- [Parameter(ParameterSetName = "install")]
335
- [string]$Target,
336
-
337
- [Parameter(ParameterSetName = "install")]
338
- [int]$IntervalMinutes = 5,
339
-
340
- [Parameter(ParameterSetName = "install")]
341
- [int]$AgeSeconds = 120,
342
-
343
- [Parameter(ParameterSetName = "remove")]
344
- [switch]$Remove,
345
-
346
- [Parameter(ParameterSetName = "status")]
347
- [switch]$Status
348
- )
349
-
350
- $TaskName = "SweepEmptyDirs"
351
-
352
- if ($Status) {
353
- $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
354
- if (-not $task) {
355
- Write-Host "STATUS: $TaskName is not registered."
356
- return
357
- }
358
- Write-Host "STATUS: $TaskName is registered."
359
- Write-Host " State: $($task.State)"
360
- Write-Host " Actions:"
361
- foreach ($action in $task.Actions) {
362
- Write-Host " $($action.Execute) $($action.Arguments)"
363
- }
364
- Write-Host " Triggers:"
365
- foreach ($trigger in $task.Triggers) {
366
- Write-Host " $($trigger.Repetition.Interval) (starting $($trigger.StartBoundary))"
367
- }
368
- return
369
- }
370
-
371
- if ($Remove) {
372
- Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
373
- Write-Host "$TaskName removed."
374
- return
375
- }
376
-
377
- $ScriptDir = Split-Path -Parent $PSCommandPath
378
- $ScriptPath = Join-Path $ScriptDir "sweep_empty_dirs.py"
379
-
380
- if (-not (Test-Path $ScriptPath)) {
381
- Write-Error "sweep_empty_dirs.py not found at: $ScriptPath"
382
- exit 1
383
- }
384
-
385
- if (-not $Target) {
386
- Write-Error "Parameter -Target is required (the directory to watch)."
387
- exit 1
388
- }
389
-
390
- if (-not (Test-Path $Target)) {
391
- Write-Error "Target directory does not exist: $Target"
392
- exit 1
393
- }
394
-
395
- $_py = Get-Command py -ErrorAction SilentlyContinue
396
- $PythonPath = if ($_py) { $_py.Source } else { (Get-Command python).Source }
397
- if (-not $PythonPath) {
398
- Write-Error "Cannot find Python (py or python) on PATH."
399
- exit 1
400
- }
401
- $Action = New-ScheduledTaskAction -Execute $PythonPath -Argument "$ScriptPath --once --age $AgeSeconds ""$Target"""
402
- $Trigger = New-ScheduledTaskTrigger -Daily -At "00:00" -RepetitionInterval (New-TimeSpan -Minutes $IntervalMinutes)
403
- $Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
404
-
405
- Register-ScheduledTask -TaskName $TaskName -Action $Action -Trigger $Trigger -Settings $Settings -Force | Out-Null
406
- Write-Host "$TaskName registered — runs every ${IntervalMinutes}min against '$Target' (age ≥ ${AgeSeconds}s)."
407
- ```