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 CHANGED
@@ -1,13 +1,19 @@
1
1
  # Claude Development Assistant
2
2
 
3
- The user is short on time and tokens. 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.
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
- ## Code Rules
10
- @~/.claude/docs/CODE_RULES.md
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 tool-less `code-advisor` agent which returns a plan, a correction, or a stop signal — and resumes. Source: Anthropic's advisor strategy (https://claude.com/blog/the-advisor-strategy).
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 run_staged_test_files(repository_root: Path) -> int:
1563
- """Run pytest over the staged test files and return the gate exit code.
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
- repository_root: The repository root the staged test files belong to.
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
- 0 when no test file is staged, when the staged test files collect
1570
- no tests (pytest's no-tests-collected exit code), or when every staged
1571
- test passes. pytest's non-zero exit code otherwise, which blocks the
1572
- commit.
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 all_test_paths],
1638
+ *[str(each_path) for each_path in all_group_test_paths],
1582
1639
  ],
1583
- cwd=str(repository_root),
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
@@ -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: inherit
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'
@@ -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 |