claude-dev-env 1.88.1 → 1.90.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +13 -34
- package/_shared/pr-loop/scripts/code_rules_gate.py +106 -14
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +26 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +160 -0
- package/agents/code-verifier.md +2 -1
- package/bin/install.mjs +0 -1
- package/hooks/blocking/CLAUDE.md +1 -0
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +210 -0
- package/hooks/blocking/volatile_path_in_post_blocker.py +351 -0
- package/hooks/hooks.json +25 -0
- package/hooks/hooks_constants/CLAUDE.md +2 -0
- package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +48 -0
- package/hooks/lifecycle/CLAUDE.md +3 -1
- package/hooks/lifecycle/enter_worktree_origin_prefetch.py +146 -0
- package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +178 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/durable-post-artifacts.md +35 -0
- package/scripts/CLAUDE.md +1 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -0
- package/scripts/dev_env_scripts_constants/gh_artifact_upload_constants.py +43 -0
- package/scripts/gh_artifact_upload.py +256 -0
- package/scripts/tests/test_gh_artifact_upload.py +205 -0
- package/skills/CLAUDE.md +0 -2
- package/skills/advisor/SKILL.md +0 -185
- package/skills/advisor-refresh/SKILL.md +0 -25
package/CLAUDE.md
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
# Claude Development Assistant
|
|
2
2
|
|
|
3
|
-
The user is short on time and
|
|
4
|
-
|
|
5
|
-
The user is short on tokens; whenever a task can be achieved cleanly and effectively, optimize to save the user $$ and token usage.
|
|
3
|
+
The user is short on time and appreciates brevity in replies. When you reply, always assume they'll only read your first few sentences and final sentences. Anything else is skimmed at best. Frame your replies accordingly.
|
|
6
4
|
|
|
7
5
|
The user delegates execution to you and expects zero manual steps unless strictly necessary. Execute every command you can directly. Only instruct the user to do something manually when you are technically unable to do it yourself. When a task involves credentials or other sensitive input, display a minimal secure UI (e.g., a password dialog) to collect it rather than asking the user to paste it into chat or run the command themselves. When direction is ambiguous, use AskUserQuestion to clarify before acting.
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
You have access to an `advisor` tool backed by a stronger reviewer model. It takes NO parameters — when you call advisor(), your entire conversation history is automatically forwarded. They see the task, every tool call you've made, every result you've seen.
|
|
8
|
+
|
|
9
|
+
Call advisor BEFORE substantive work — before writing, before committing to an interpretation, before building on an assumption. If the task requires orientation first (finding files, fetching a source, seeing what's there), do that, then call advisor. Orientation is not substantive work. Writing, editing, and declaring an answer are.
|
|
10
|
+
|
|
11
|
+
Also call advisor:
|
|
12
|
+
- When you believe the task is complete. BEFORE this call, make your deliverable durable: write the file, save the result, commit the change. The advisor call takes time; if the session ends during it, a durable result persists and an unwritten one doesn't.
|
|
13
|
+
- When stuck — errors recurring, approach not converging, results that don't fit.
|
|
14
|
+
- When considering a change of approach.
|
|
15
|
+
|
|
16
|
+
On tasks longer than a few steps, call advisor at least once before committing to an approach and once before declaring done. On short reactive tasks where the next action is dictated by tool output you just read, you don't need to keep calling — the advisor adds most of its value on the first call, before the approach crystallizes.
|
|
11
17
|
|
|
12
18
|
ALWAYS call the AskUserQuestion tool if you have a question for the user. Provide content-appropriate default options, with a flag for the recommended one.
|
|
13
19
|
|
|
@@ -21,12 +27,8 @@ Every Markdown file I write or edit describes the system's **current** state onl
|
|
|
21
27
|
Full banned-pattern set + enforcement: `~/.claude/rules/no-historical-clutter.md` (hook `state-description-blocker`) and `~/.claude/rules/self-contained-docs.md`.
|
|
22
28
|
|
|
23
29
|
## GOTCHAS
|
|
24
|
-
ALWAYS base new worktrees off of UPSTREAM origin main. Assume local is out of date.
|
|
25
|
-
|
|
26
30
|
ALWAYS start each session with a /loop 15m populate or update the task list based on remaining todos.
|
|
27
31
|
|
|
28
|
-
When making code changes, make sure you are working in the proper worktree path for the task at hand.
|
|
29
|
-
|
|
30
32
|
## Choosing Edit vs Write
|
|
31
33
|
|
|
32
34
|
`Edit` changes existing files; `Write` creates new ones. Default to `Edit` — reach for `Write` only for a genuinely new path. For a true full rewrite, delete the file first, then `Write`.
|
|
@@ -37,10 +39,6 @@ When I ask you to "show me", "open", "display", "let me see", or "pull up" a fil
|
|
|
37
39
|
|
|
38
40
|
`Start-Process pwsh -WindowStyle Hidden -ArgumentList '-NoProfile','-File',"$HOME\.claude\scripts\Show-Asset.ps1",'<path 1>','<path 2>'`
|
|
39
41
|
|
|
40
|
-
It sizes each image window to the image (scaled down to fit the screen) and opens non-image files in their default app; pass every path I name. Printing a path or attaching the file is not showing it — do that only when the file truly cannot be opened, and say why.
|
|
41
|
-
|
|
42
|
-
The `send_user_file_open_locally_blocker` hook backs this up: it blocks a desk-side `SendUserFile` attach and sends you back to this command, while a phone push (`status: "proactive"`) stays allowed.
|
|
43
|
-
|
|
44
42
|
## Test Philosophy
|
|
45
43
|
|
|
46
44
|
When writing tests, always write tests that actually test the behavior of the function against actual, real data and environments.
|
|
@@ -59,34 +57,15 @@ Reserve `Read`/`Grep`/`Glob` for files you will actually touch this turn. Compos
|
|
|
59
57
|
|
|
60
58
|
Run every multi-step code task in two phases:
|
|
61
59
|
|
|
62
|
-
1. **Coders** — one coder agent per scoped assignment writes the code. A coder that hits a decision it can't reasonably solve consults the
|
|
63
|
-
2. **Verification** — when the coders finish, the main session spawns the `code-verifier` agent in a fresh context. It derives and runs the checks itself rather than trusting coder reports: the task's named gates, tests against baselines recorded before the coders ran, and a two-way diff-vs-assignment reading (every task item maps to a hunk, every hunk maps to a task item, nothing missing). A finding must cite a failing command or a named task item. Source: the fresh-context review step in Claude Code best practices (https://code.claude.com/docs/en/best-practices) — the agent doing the work isn't the one grading it.
|
|
60
|
+
1. **Coders** — one coder agent per scoped assignment writes the code. A coder that hits a decision it can't reasonably solve consults the advisor (see beginning of this file).
|
|
61
|
+
2. **Verification** — when the coders finish, the main session spawns the `code-verifier` agent in a fresh context, but you must first verify that their work is based on upstream's origin main (aka: the commit live on github). It derives and runs the checks itself rather than trusting coder reports: the task's named gates, tests against baselines recorded before the coders ran, and a two-way diff-vs-assignment reading (every task item maps to a hunk, every hunk maps to a task item, nothing missing). A finding must cite a failing command or a named task item. Source: the fresh-context review step in Claude Code best practices (https://code.claude.com/docs/en/best-practices) — the agent doing the work isn't the one grading it.
|
|
64
62
|
|
|
65
63
|
Repair agents run only on reported findings; the verifier re-checks after each repair. Work lands (commit, push, draft PR) only on a clean verdict — enforced by the `verified_commit_gate` hook, which blocks `git commit`/`git push` unless a hook-minted verdict covers the current branch diff. The one exemption is mechanical, not discretionary: a diff whose every changed file is non-code or has an unchanged Python AST once docstrings are stripped (docs, docstrings, comments).
|
|
66
64
|
|
|
67
|
-
## Converge & Review Loop Discipline
|
|
68
|
-
|
|
69
|
-
- **Worktree isolation:** Run every PR convergence and review loop in an isolated worktree, never a shared checkout that concurrent processes may advance. Verify isolation (the working directory path includes `.claude/worktrees/`) before the first tick or round.
|
|
70
|
-
- **No hedging in findings:** Findings and PR reports state verified facts only — never `likely`, `probably`, `should`, `appears to`. Verify each claim against the code before stating it; the anti-hallucination Stop hook rejects hedged responses.
|
|
71
|
-
- **Tight edit scope:** Edit exactly what the task names — no whole-file rewrites, no renaming public method parameters, no changes beyond the stated task. When the user asks for a "lasting" or "reusable" fix, prefer the durable systemic fix over a one-off edit. When the task touches a pipeline, generator, or other repeated process, fix the process itself, not its individual outputs — even when the request does not say so; for one-off targets, a scoped patch remains the default.
|
|
72
|
-
- **GitHub MCP first:** The GitHub MCP (`mcp__plugin_github_github__*`) is the primary path for PR and review-thread inspection; raw `gh api` is the fallback, not the default — MCP calls work the same from any worktree.
|
|
73
|
-
|
|
74
|
-
## Destructive-command literals in Bash
|
|
75
|
-
|
|
76
|
-
Never put a destructive-command literal (`rm -rf`, `git reset --hard`, `dd`, `mkfs`) inside a Bash command string, even when the shell never runs it — a quoted `python -c` argument, a heredoc body, an echoed string, a commit or PR body. The `destructive_command_blocker` hook matches the raw text and asks for confirmation, which a background run cannot answer, so the call stalls. Run hook and deletion checks through the committed test suite (`python -m pytest <test_file>`), or a throwaway script under `$CLAUDE_JOB_DIR/tmp` run as `python <file>.py` — either way the command string carries no destructive text, so the hook stays silent. Group genuine cleanup deletions into one teardown step. See `~/.claude/rules/no-inline-destructive-literals.md`.
|
|
77
|
-
|
|
78
65
|
## Sub-agent Output Validation
|
|
79
66
|
|
|
80
67
|
After any sub-agent returns a PR description, file list, or counts, verify each claim against the actual diff and repo state before using it. Flag and correct any invented paths, fabricated counts, or out-of-scope changes before they land in commits or PR bodies.
|
|
81
68
|
|
|
82
|
-
## Git Sync Intent
|
|
83
|
-
|
|
84
|
-
When asked to sync git ("get X onto origin main", "update main"), fast-forward local main to origin — do NOT commit untracked working-tree files unless explicitly told to.
|
|
85
|
-
|
|
86
|
-
## Scheduled Task Cadence
|
|
87
|
-
|
|
88
|
-
For scheduled/cron tasks, default to sub-hour intervals (30-minute); do not propose hourly cadences.
|
|
89
|
-
|
|
90
69
|
## Task Tracking
|
|
91
70
|
|
|
92
71
|
Track every task with the task tool, always — for all sessions and all tasks. Capture each task with `TaskCreate` as it arrives, mark it `in_progress` with `TaskUpdate` when you start, and `completed` when it is done. Run `/task-build` to gather any open tasks and add them to the list in one pass.
|
|
@@ -15,6 +15,7 @@ from pr_loop_shared_constants.code_rules_gate_constants import ( # noqa: E402
|
|
|
15
15
|
ALL_CODE_FILE_EXTENSIONS,
|
|
16
16
|
ALL_GIT_DIFF_CACHED_NAME_ONLY_NULL_TERMINATED_COMMAND,
|
|
17
17
|
ALL_GIT_DIFF_NAME_ONLY_NULL_TERMINATED_COMMAND_PREFIX,
|
|
18
|
+
ALL_PYTEST_CONFIG_FILE_SECTIONS,
|
|
18
19
|
ALL_PYTEST_MODULE_INVOCATION,
|
|
19
20
|
ALL_TEST_FILENAME_GLOB_SUFFIXES,
|
|
20
21
|
ALL_TEST_FILENAME_SUFFIXES,
|
|
@@ -38,6 +39,7 @@ from pr_loop_shared_constants.code_rules_gate_constants import ( # noqa: E402
|
|
|
38
39
|
PYTHON_FILE_EXTENSION,
|
|
39
40
|
STAGED_PYTEST_TIMEOUT_SECONDS,
|
|
40
41
|
STAGED_TEST_FAILURE_HEADER,
|
|
42
|
+
STAGED_TEST_GROUP_FAILURE_MESSAGE,
|
|
41
43
|
TEST_CONFTEST_FILENAME,
|
|
42
44
|
TEST_FILENAME_PREFIX,
|
|
43
45
|
TESTS_PATH_SEGMENT,
|
|
@@ -1559,39 +1561,129 @@ def _staged_test_file_paths(repository_root: Path) -> list[Path]:
|
|
|
1559
1561
|
return all_test_paths
|
|
1560
1562
|
|
|
1561
1563
|
|
|
1562
|
-
def
|
|
1563
|
-
"""
|
|
1564
|
+
def _directory_holds_pytest_config(directory: Path) -> bool:
|
|
1565
|
+
"""Return True when *directory* holds a recognized pytest configuration file."""
|
|
1566
|
+
for each_filename, each_required_section in ALL_PYTEST_CONFIG_FILE_SECTIONS:
|
|
1567
|
+
config_path = directory / each_filename
|
|
1568
|
+
if not config_path.is_file():
|
|
1569
|
+
continue
|
|
1570
|
+
if each_required_section is None:
|
|
1571
|
+
return True
|
|
1572
|
+
try:
|
|
1573
|
+
config_text = config_path.read_text(encoding="utf-8", errors="replace")
|
|
1574
|
+
except OSError:
|
|
1575
|
+
continue
|
|
1576
|
+
if each_required_section in config_text:
|
|
1577
|
+
return True
|
|
1578
|
+
return False
|
|
1579
|
+
|
|
1580
|
+
|
|
1581
|
+
def _resolve_owning_test_root(test_file_path: Path, repository_root: Path) -> Path:
|
|
1582
|
+
"""Return the nearest ancestor of *test_file_path* that owns a pytest config.
|
|
1583
|
+
|
|
1584
|
+
Walks up from the test file to the first directory holding a pytest config,
|
|
1585
|
+
stopping at *repository_root*, which is the fallback when none is found.
|
|
1564
1586
|
|
|
1565
1587
|
Args:
|
|
1566
|
-
|
|
1588
|
+
test_file_path: The staged test file to resolve a root for.
|
|
1589
|
+
repository_root: The repository root that bounds the upward walk.
|
|
1567
1590
|
|
|
1568
1591
|
Returns:
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1592
|
+
The resolved owning test root directory.
|
|
1593
|
+
"""
|
|
1594
|
+
resolved_repository_root = repository_root.resolve()
|
|
1595
|
+
for each_ancestor in test_file_path.resolve().parents:
|
|
1596
|
+
if _directory_holds_pytest_config(each_ancestor):
|
|
1597
|
+
return each_ancestor
|
|
1598
|
+
if each_ancestor == resolved_repository_root:
|
|
1599
|
+
return resolved_repository_root
|
|
1600
|
+
return resolved_repository_root
|
|
1601
|
+
|
|
1602
|
+
|
|
1603
|
+
def _group_staged_tests_by_root(
|
|
1604
|
+
all_test_paths: list[Path],
|
|
1605
|
+
repository_root: Path,
|
|
1606
|
+
) -> dict[Path, list[Path]]:
|
|
1607
|
+
"""Group *all_test_paths* by their owning pytest-config root.
|
|
1608
|
+
|
|
1609
|
+
Args:
|
|
1610
|
+
all_test_paths: The staged test files to partition.
|
|
1611
|
+
repository_root: The repository root that bounds each upward walk.
|
|
1612
|
+
|
|
1613
|
+
Returns:
|
|
1614
|
+
A mapping from owning test root to the staged test files under it.
|
|
1615
|
+
"""
|
|
1616
|
+
tests_by_root: dict[Path, list[Path]] = {}
|
|
1617
|
+
for each_test_path in all_test_paths:
|
|
1618
|
+
owning_root = _resolve_owning_test_root(each_test_path, repository_root)
|
|
1619
|
+
tests_by_root.setdefault(owning_root, []).append(each_test_path)
|
|
1620
|
+
return tests_by_root
|
|
1621
|
+
|
|
1622
|
+
|
|
1623
|
+
def _run_pytest_for_group(group_root: Path, all_group_test_paths: list[Path]) -> int:
|
|
1624
|
+
"""Run pytest over one group's test files with the working directory at its root.
|
|
1625
|
+
|
|
1626
|
+
Args:
|
|
1627
|
+
group_root: The owning test root used as the pytest working directory.
|
|
1628
|
+
all_group_test_paths: The staged test files that share *group_root*.
|
|
1629
|
+
|
|
1630
|
+
Returns:
|
|
1631
|
+
0 when the group's tests pass or collect nothing; pytest's non-zero
|
|
1632
|
+
exit code otherwise.
|
|
1573
1633
|
"""
|
|
1574
|
-
all_test_paths = _staged_test_file_paths(repository_root)
|
|
1575
|
-
if not all_test_paths:
|
|
1576
|
-
return 0
|
|
1577
1634
|
pytest_process = subprocess.run(
|
|
1578
1635
|
[
|
|
1579
1636
|
sys.executable,
|
|
1580
1637
|
*ALL_PYTEST_MODULE_INVOCATION,
|
|
1581
|
-
*[str(each_path) for each_path in
|
|
1638
|
+
*[str(each_path) for each_path in all_group_test_paths],
|
|
1582
1639
|
],
|
|
1583
|
-
cwd=str(
|
|
1640
|
+
cwd=str(group_root),
|
|
1584
1641
|
timeout=STAGED_PYTEST_TIMEOUT_SECONDS,
|
|
1585
1642
|
check=False,
|
|
1586
1643
|
env=repository_environment(),
|
|
1587
1644
|
)
|
|
1588
1645
|
if pytest_process.returncode == PYTEST_NO_TESTS_COLLECTED_EXIT_CODE:
|
|
1589
1646
|
return 0
|
|
1590
|
-
if pytest_process.returncode != 0:
|
|
1591
|
-
print(STAGED_TEST_FAILURE_HEADER, file=sys.stderr)
|
|
1592
1647
|
return pytest_process.returncode
|
|
1593
1648
|
|
|
1594
1649
|
|
|
1650
|
+
def run_staged_test_files(repository_root: Path) -> int:
|
|
1651
|
+
"""Run pytest over the staged test files and return the gate exit code.
|
|
1652
|
+
|
|
1653
|
+
Groups the staged test files by their owning pytest-config root and runs one
|
|
1654
|
+
pytest session per group with the working directory at that root, so two
|
|
1655
|
+
packages that expose a same-named top-level package never share one session
|
|
1656
|
+
and shadow each other's imports.
|
|
1657
|
+
|
|
1658
|
+
Args:
|
|
1659
|
+
repository_root: The repository root the staged test files belong to.
|
|
1660
|
+
|
|
1661
|
+
Returns:
|
|
1662
|
+
0 when no test file is staged, when every group collects no tests, or
|
|
1663
|
+
when every group passes. The first failing group's non-zero pytest exit
|
|
1664
|
+
code otherwise, which blocks the commit.
|
|
1665
|
+
"""
|
|
1666
|
+
all_test_paths = _staged_test_file_paths(repository_root)
|
|
1667
|
+
if not all_test_paths:
|
|
1668
|
+
return 0
|
|
1669
|
+
tests_by_root = _group_staged_tests_by_root(all_test_paths, repository_root)
|
|
1670
|
+
first_failing_exit_code = 0
|
|
1671
|
+
for each_group_root in sorted(tests_by_root):
|
|
1672
|
+
group_exit_code = _run_pytest_for_group(
|
|
1673
|
+
each_group_root, tests_by_root[each_group_root]
|
|
1674
|
+
)
|
|
1675
|
+
if group_exit_code != 0:
|
|
1676
|
+
print(
|
|
1677
|
+
STAGED_TEST_GROUP_FAILURE_MESSAGE.format(group_root=each_group_root),
|
|
1678
|
+
file=sys.stderr,
|
|
1679
|
+
)
|
|
1680
|
+
if first_failing_exit_code == 0:
|
|
1681
|
+
first_failing_exit_code = group_exit_code
|
|
1682
|
+
if first_failing_exit_code != 0:
|
|
1683
|
+
print(STAGED_TEST_FAILURE_HEADER, file=sys.stderr)
|
|
1684
|
+
return first_failing_exit_code
|
|
1685
|
+
|
|
1686
|
+
|
|
1595
1687
|
def _report_terminology_findings(all_findings: list[str]) -> None:
|
|
1596
1688
|
"""Print the terminology-sweep findings, when any, to standard error.
|
|
1597
1689
|
|
|
@@ -93,3 +93,29 @@ STAGED_PYTEST_TIMEOUT_SECONDS: int = 600
|
|
|
93
93
|
STAGED_TEST_FAILURE_HEADER: str = (
|
|
94
94
|
"code_rules_gate: staged test file(s) failed under pytest; commit blocked."
|
|
95
95
|
)
|
|
96
|
+
|
|
97
|
+
PYTEST_INI_FILENAME: str = "pytest.ini"
|
|
98
|
+
|
|
99
|
+
PYPROJECT_TOML_FILENAME: str = "pyproject.toml"
|
|
100
|
+
|
|
101
|
+
SETUP_CFG_FILENAME: str = "setup.cfg"
|
|
102
|
+
|
|
103
|
+
TOX_INI_FILENAME: str = "tox.ini"
|
|
104
|
+
|
|
105
|
+
PYPROJECT_PYTEST_CONFIG_SECTION: str = "[tool.pytest.ini_options]"
|
|
106
|
+
|
|
107
|
+
SETUP_CFG_PYTEST_CONFIG_SECTION: str = "[tool:pytest]"
|
|
108
|
+
|
|
109
|
+
TOX_INI_PYTEST_CONFIG_SECTION: str = "[pytest]"
|
|
110
|
+
|
|
111
|
+
ALL_PYTEST_CONFIG_FILE_SECTIONS: tuple[tuple[str, str | None], ...] = (
|
|
112
|
+
(PYTEST_INI_FILENAME, None),
|
|
113
|
+
(PYPROJECT_TOML_FILENAME, PYPROJECT_PYTEST_CONFIG_SECTION),
|
|
114
|
+
(SETUP_CFG_FILENAME, SETUP_CFG_PYTEST_CONFIG_SECTION),
|
|
115
|
+
(TOX_INI_FILENAME, TOX_INI_PYTEST_CONFIG_SECTION),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
STAGED_TEST_GROUP_FAILURE_MESSAGE: str = (
|
|
119
|
+
"code_rules_gate: staged test group rooted at {group_root} "
|
|
120
|
+
"failed under pytest; commit blocked."
|
|
121
|
+
)
|
|
@@ -2418,3 +2418,163 @@ def test_run_staged_test_files_runs_pytest_without_git_environment(
|
|
|
2418
2418
|
monkeypatch.setenv("GIT_AUTHOR_NAME", "Hostile Hook Environment")
|
|
2419
2419
|
|
|
2420
2420
|
assert gate_module.run_staged_test_files(repository_root) == 0
|
|
2421
|
+
|
|
2422
|
+
|
|
2423
|
+
def _build_shadowing_skill(
|
|
2424
|
+
repository_root: Path,
|
|
2425
|
+
skill_name: str,
|
|
2426
|
+
module_name: str,
|
|
2427
|
+
) -> Path:
|
|
2428
|
+
skill_directory = repository_root / skill_name
|
|
2429
|
+
write_file(skill_directory / "pytest.ini", "[pytest]\n")
|
|
2430
|
+
write_file(skill_directory / "scripts" / "__init__.py", "")
|
|
2431
|
+
write_file(
|
|
2432
|
+
skill_directory / "scripts" / f"{module_name}.py",
|
|
2433
|
+
f"{module_name}_marker = 1\n",
|
|
2434
|
+
)
|
|
2435
|
+
test_relative_path = f"{skill_name}/test_{module_name}.py"
|
|
2436
|
+
write_file(
|
|
2437
|
+
repository_root / test_relative_path,
|
|
2438
|
+
f"from scripts.{module_name} import {module_name}_marker\n"
|
|
2439
|
+
"\n"
|
|
2440
|
+
"\n"
|
|
2441
|
+
f"def test_{module_name}_marker_is_one() -> None:\n"
|
|
2442
|
+
f" assert {module_name}_marker == 1\n",
|
|
2443
|
+
)
|
|
2444
|
+
stage_file(repository_root, test_relative_path)
|
|
2445
|
+
return repository_root / test_relative_path
|
|
2446
|
+
|
|
2447
|
+
|
|
2448
|
+
def test_run_staged_test_files_passes_when_same_named_package_would_shadow(
|
|
2449
|
+
temporary_git_repository: Path,
|
|
2450
|
+
) -> None:
|
|
2451
|
+
"""Two skill roots each own a top-level ``scripts`` package. One combined
|
|
2452
|
+
pytest session binds ``scripts`` to whichever imports first and the other
|
|
2453
|
+
skill's ``from scripts.<module>`` then raises ImportError. Per-root sessions
|
|
2454
|
+
keep each ``scripts`` isolated, so the gate passes."""
|
|
2455
|
+
first_test_path = _build_shadowing_skill(
|
|
2456
|
+
temporary_git_repository, "skill_alpha", "widget"
|
|
2457
|
+
)
|
|
2458
|
+
second_test_path = _build_shadowing_skill(
|
|
2459
|
+
temporary_git_repository, "skill_beta", "gadget"
|
|
2460
|
+
)
|
|
2461
|
+
|
|
2462
|
+
combined_session = subprocess.run(
|
|
2463
|
+
[sys.executable, "-m", "pytest", "-q", str(first_test_path), str(second_test_path)],
|
|
2464
|
+
cwd=str(temporary_git_repository),
|
|
2465
|
+
capture_output=True,
|
|
2466
|
+
text=True,
|
|
2467
|
+
encoding="utf-8",
|
|
2468
|
+
errors="replace",
|
|
2469
|
+
check=False,
|
|
2470
|
+
)
|
|
2471
|
+
assert combined_session.returncode != 0, (
|
|
2472
|
+
"one combined pytest session must fail on the same-named scripts package "
|
|
2473
|
+
"shadow; if it does not, the shadow scenario is not being reproduced"
|
|
2474
|
+
)
|
|
2475
|
+
|
|
2476
|
+
assert gate_module.run_staged_test_files(temporary_git_repository) == 0, (
|
|
2477
|
+
"per-root pytest sessions must keep each scripts package isolated so both "
|
|
2478
|
+
"staged suites pass"
|
|
2479
|
+
)
|
|
2480
|
+
|
|
2481
|
+
|
|
2482
|
+
def test_resolve_owning_test_root_returns_pyproject_config_directory(
|
|
2483
|
+
tmp_path: Path,
|
|
2484
|
+
) -> None:
|
|
2485
|
+
package_root = tmp_path / "package_root"
|
|
2486
|
+
write_file(
|
|
2487
|
+
package_root / "pyproject.toml",
|
|
2488
|
+
"[tool.pytest.ini_options]\ntestpaths = ['tests']\n",
|
|
2489
|
+
)
|
|
2490
|
+
test_path = package_root / "tests" / "test_behavior.py"
|
|
2491
|
+
write_file(test_path, "def test_behavior() -> None:\n assert True\n")
|
|
2492
|
+
|
|
2493
|
+
resolved_root = gate_module._resolve_owning_test_root(test_path, tmp_path)
|
|
2494
|
+
|
|
2495
|
+
assert resolved_root == package_root.resolve()
|
|
2496
|
+
|
|
2497
|
+
|
|
2498
|
+
def test_resolve_owning_test_root_returns_pytest_ini_directory(
|
|
2499
|
+
tmp_path: Path,
|
|
2500
|
+
) -> None:
|
|
2501
|
+
package_root = tmp_path / "package_root"
|
|
2502
|
+
write_file(package_root / "pytest.ini", "[pytest]\n")
|
|
2503
|
+
test_path = package_root / "suite" / "test_behavior.py"
|
|
2504
|
+
write_file(test_path, "def test_behavior() -> None:\n assert True\n")
|
|
2505
|
+
|
|
2506
|
+
resolved_root = gate_module._resolve_owning_test_root(test_path, tmp_path)
|
|
2507
|
+
|
|
2508
|
+
assert resolved_root == package_root.resolve()
|
|
2509
|
+
|
|
2510
|
+
|
|
2511
|
+
def test_resolve_owning_test_root_falls_back_to_repository_root(
|
|
2512
|
+
tmp_path: Path,
|
|
2513
|
+
) -> None:
|
|
2514
|
+
test_path = tmp_path / "nested" / "deeper" / "test_behavior.py"
|
|
2515
|
+
write_file(test_path, "def test_behavior() -> None:\n assert True\n")
|
|
2516
|
+
|
|
2517
|
+
resolved_root = gate_module._resolve_owning_test_root(test_path, tmp_path)
|
|
2518
|
+
|
|
2519
|
+
assert resolved_root == tmp_path.resolve()
|
|
2520
|
+
|
|
2521
|
+
|
|
2522
|
+
def test_resolve_owning_test_root_skips_pyproject_without_pytest_section(
|
|
2523
|
+
tmp_path: Path,
|
|
2524
|
+
) -> None:
|
|
2525
|
+
outer_root = tmp_path / "outer"
|
|
2526
|
+
write_file(
|
|
2527
|
+
outer_root / "pyproject.toml",
|
|
2528
|
+
"[tool.pytest.ini_options]\ntestpaths = ['tests']\n",
|
|
2529
|
+
)
|
|
2530
|
+
write_file(outer_root / "inner" / "pyproject.toml", "[project]\nname = 'inner'\n")
|
|
2531
|
+
test_path = outer_root / "inner" / "test_behavior.py"
|
|
2532
|
+
write_file(test_path, "def test_behavior() -> None:\n assert True\n")
|
|
2533
|
+
|
|
2534
|
+
resolved_root = gate_module._resolve_owning_test_root(test_path, tmp_path)
|
|
2535
|
+
|
|
2536
|
+
assert resolved_root == outer_root.resolve()
|
|
2537
|
+
|
|
2538
|
+
|
|
2539
|
+
def test_group_staged_tests_by_root_shares_a_session_for_same_root(
|
|
2540
|
+
tmp_path: Path,
|
|
2541
|
+
) -> None:
|
|
2542
|
+
package_root = tmp_path / "package_root"
|
|
2543
|
+
write_file(package_root / "pytest.ini", "[pytest]\n")
|
|
2544
|
+
first_test_path = package_root / "tests" / "test_first.py"
|
|
2545
|
+
second_test_path = package_root / "tests" / "test_second.py"
|
|
2546
|
+
write_file(first_test_path, "def test_first() -> None:\n assert True\n")
|
|
2547
|
+
write_file(second_test_path, "def test_second() -> None:\n assert True\n")
|
|
2548
|
+
|
|
2549
|
+
tests_by_root = gate_module._group_staged_tests_by_root(
|
|
2550
|
+
[first_test_path, second_test_path], tmp_path
|
|
2551
|
+
)
|
|
2552
|
+
|
|
2553
|
+
assert set(tests_by_root) == {package_root.resolve()}
|
|
2554
|
+
assert sorted(tests_by_root[package_root.resolve()]) == sorted(
|
|
2555
|
+
[first_test_path, second_test_path]
|
|
2556
|
+
)
|
|
2557
|
+
|
|
2558
|
+
|
|
2559
|
+
def test_run_staged_test_files_fails_when_one_group_fails(
|
|
2560
|
+
temporary_git_repository: Path,
|
|
2561
|
+
capsys: pytest.CaptureFixture[str],
|
|
2562
|
+
) -> None:
|
|
2563
|
+
write_file(temporary_git_repository / "passing_skill" / "pytest.ini", "[pytest]\n")
|
|
2564
|
+
write_file(
|
|
2565
|
+
temporary_git_repository / "passing_skill" / "test_pass.py",
|
|
2566
|
+
"def test_pass() -> None:\n assert True\n",
|
|
2567
|
+
)
|
|
2568
|
+
stage_file(temporary_git_repository, "passing_skill/test_pass.py")
|
|
2569
|
+
write_file(temporary_git_repository / "failing_skill" / "pytest.ini", "[pytest]\n")
|
|
2570
|
+
write_file(
|
|
2571
|
+
temporary_git_repository / "failing_skill" / "test_fail.py",
|
|
2572
|
+
"def test_fail() -> None:\n assert False\n",
|
|
2573
|
+
)
|
|
2574
|
+
stage_file(temporary_git_repository, "failing_skill/test_fail.py")
|
|
2575
|
+
|
|
2576
|
+
exit_code = gate_module.run_staged_test_files(temporary_git_repository)
|
|
2577
|
+
|
|
2578
|
+
assert exit_code != 0
|
|
2579
|
+
captured_error = capsys.readouterr().err
|
|
2580
|
+
assert "failing_skill" in captured_error
|
package/agents/code-verifier.md
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
name: code-verifier
|
|
3
3
|
description: Post-hoc verification agent for the two-phase code workflow. Spawned by the main session after coder agents finish. Runs every check itself in a fresh context — named gates, tests against recorded baselines, two-way diff-vs-task reading — and ends with a fenced verdict block the verifier_verdict_minter hook turns into the commit-gate verdict. Read and execute only; it never edits files.
|
|
4
4
|
tools: Read, Grep, Glob, Bash
|
|
5
|
-
model:
|
|
5
|
+
model: sonnet
|
|
6
|
+
effort: medium
|
|
6
7
|
color: orange
|
|
7
8
|
---
|
|
8
9
|
|
package/bin/install.mjs
CHANGED
|
@@ -147,7 +147,6 @@ const INSTALL_GROUPS = {
|
|
|
147
147
|
core: {
|
|
148
148
|
description: 'Development standards, hooks, agents, commands',
|
|
149
149
|
skills: [
|
|
150
|
-
'advisor', 'advisor-refresh',
|
|
151
150
|
'anthropic-plan', 'everything-search',
|
|
152
151
|
'pr-review-responder',
|
|
153
152
|
'recall', 'remember', 'task-build', 'verified-build'
|
package/hooks/blocking/CLAUDE.md
CHANGED
|
@@ -98,6 +98,7 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
|
|
|
98
98
|
| `verified_commit_gate.py` | PreToolUse (Bash/PowerShell) | `git commit`/`git push` without a passing verifier verdict |
|
|
99
99
|
| `verified_commit_message_accuracy_blocker.py` | PreToolUse | Commit messages that misstate what the diff has |
|
|
100
100
|
| `verifier_verdict_minter.py` | SubagentStop | Mints a passing verdict file when a `code-verifier` agent finishes cleanly |
|
|
101
|
+
| `volatile_path_in_post_blocker.py` | PreToolUse (Bash/MCP GitHub) | `gh` post commands and GitHub MCP post tools whose body references a volatile path (job scratch dir, worktree, or system temp) that outlives the durable post |
|
|
101
102
|
| `windows_rmtree_blocker.py` | PreToolUse (Write/Edit) | `shutil.rmtree` with `ignore_errors=True` on Windows |
|
|
102
103
|
| `workflow_substitution_slot_blocker.py` | PreToolUse (Write/Edit) | Workflow templates with bare per-iteration tokens missing angle-bracket slots |
|
|
103
104
|
| `write_existing_file_blocker.py` | PreToolUse (Write) | Write to a path where a file already exists |
|