Skip to content

Commit 085d672

Browse files
authored
Merge pull request #7 from alexfazio/feat-setup-wizard-rerun-config-editor
Add rerun wizard config editor and guided pre-commit setup
2 parents be8926e + 31acb3d commit 085d672

5 files changed

Lines changed: 439 additions & 31 deletions

File tree

docs/psf/00-plankton-architecture-overview.md

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ graph TB
165165

166166
### multi_linter.sh (PostToolUse Hook)
167167

168-
- **Location**: `.claude/hooks/multi_linter.sh` (~1,606 lines)
168+
- **Location**: `.claude/hooks/multi_linter.sh` (~1,677 lines)
169169
- **Responsibilities**:
170170
- Dispatches files to language-specific handlers based on extension
171171
- Runs three-phase lint: format, collect
@@ -188,7 +188,7 @@ graph TB
188188

189189
### protect_linter_configs.sh (PreToolUse Hook)
190190

191-
- **Location**: `.claude/hooks/protect_linter_configs.sh` (~87 lines)
191+
- **Location**: `.claude/hooks/protect_linter_configs.sh` (~164 lines)
192192
- **Responsibilities**: Blocks Edit/Write on protected config files and hook scripts
193193
- **Implementation**: Extracts file path from stdin
194194
JSON, matches against `config.json` protected list
@@ -198,7 +198,7 @@ graph TB
198198

199199
### stop_config_guardian.sh (Stop Hook)
200200

201-
- **Location**: `.claude/hooks/stop_config_guardian.sh` (~158 lines)
201+
- **Location**: `.claude/hooks/stop_config_guardian.sh` (~162 lines)
202202
- **Responsibilities**: Detects modified config files
203203
at session end; prompts user to keep or restore
204204
- **Implementation**: `git diff --name-only` (no LLM).
@@ -230,7 +230,7 @@ graph TB
230230

231231
### enforce_package_managers.sh (PreToolUse Hook)
232232

233-
- **Location**: `.claude/hooks/enforce_package_managers.sh` (~512 lines)
233+
- **Location**: `.claude/hooks/enforce_package_managers.sh` (~705 lines)
234234
- **Responsibilities**: Intercepts legacy package manager
235235
commands in Bash tool and blocks or warns, suggesting
236236
project-preferred alternatives (uv for Python, bun for JS)
@@ -245,7 +245,7 @@ graph TB
245245

246246
### test_hook.sh (Debug/Test Utility)
247247

248-
- **Location**: `.claude/hooks/test_hook.sh` (~2,012 lines)
248+
- **Location**: `.claude/hooks/test_hook.sh` (~2,089 lines)
249249
- **Responsibilities**: Self-test suite covering all
250250
file types, model selection, TS handling, config
251251
protection, and edge cases
@@ -334,7 +334,7 @@ CI pipeline, and 303+ automated checks.
334334
- **Quick reference**: `bash .claude/hooks/test_hook.sh --self-test`
335335
(113 cases), `bash .claude/tests/hooks/verify_feedback_loop.sh`
336336
(28 checks), `bash tests/stress/run_stress_tests.sh` (133 tests),
337-
`.venv/bin/pytest tests/` (297 tests: 267 unit + 30 integration);
337+
`.venv/bin/pytest tests/` (345 tests: 315 unit + 30 integration);
338338
hook investigation tests: `test_nursery_config.sh` (3 tests),
339339
`test_env_propagation.sh` (3 tests), `test_subprocess_permissions.sh` (5 tests)
340340
- **Type safety**: Python 3.11+; ty in Phase 2b;
@@ -372,7 +372,7 @@ CI pipeline, and 303+ automated checks.
372372

373373
## Risks, Tech Debt, Open Questions
374374

375-
- **Shell script size**: `multi_linter.sh` ~1,486
375+
- **Shell script size**: `multi_linter.sh` ~1,677
376376
lines; per-language modules would help
377377
- **Fragile parsing**: yamllint/flake8/markdownlint
378378
output parsed via `sed`; format changes break it
@@ -397,10 +397,21 @@ for the full benchmark PSF covering all 8 modules, the CLI,
397397
- **`docs/REFERENCE.md`**: Detailed hook system documentation
398398
with architecture diagrams, message flow, configuration
399399
reference, and testing guides
400-
- **`scripts/setup.py`**: Interactive setup wizard that auto-detects
401-
project stack and generates `config.json`
402-
- **`scripts/init-typescript.sh`**: Initializes
400+
- **`scripts/setup.py`** (1,086 lines): Interactive setup wizard that
401+
auto-detects project stack, generates `config.json`, and supports
402+
rerunnable config editing with guided pre-commit setup. Validates
403+
dependencies, offers dependency installation via Homebrew/package
404+
managers, and configures TypeScript/Python/Shell linting options
405+
- **`scripts/setup.sh`** (325 lines): Non-interactive installer for
406+
CI and scripted environments; mirrors setup.py functionality
407+
- **`scripts/init-typescript.sh`** (264 lines): Initializes
403408
TypeScript support (installs Biome, creates configs)
409+
- **`scripts/pre_commit_plankton_strict.sh`** (160 lines): Strict
410+
pre-commit checks for CI enforcement
411+
- **`scripts/pre_push_plankton_hooks.sh`** (22 lines): Pre-push hook
412+
runner for git integration
413+
- **`scripts/check_commit_message_no_ai_attribution.sh`**: Commit
414+
message policy hook enforcing AI attribution rules
404415
- **`tests/stress/run_stress_tests.sh`**: Stress test
405416
suite for hook performance under load
406417
- **`.claude/tests/hooks/`**: Integration test suite

docs/psf/01-testing-infrastructure.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
markdown report generation
1616
- CI pipeline: pre-commit (lint) + pytest (test) on push/PR
1717
- **Test totals**: 113 + 28 + 10 + 20 + 133 + 11 = 315 hook checks
18-
(+ 103 agent-based integration tests + 297 pytest tests)
18+
(+ 103 agent-based integration tests + 345 pytest tests)
1919
- **Dependencies**: ShellCheck, ruff, jaq, yamllint, hadolint,
2020
taplo, markdownlint-cli2; biome optional
2121

@@ -72,7 +72,7 @@ graph TB
7272
end
7373
7474
subgraph "Layer 4: Unit Tests"
75-
UT[pytest tests/unit/<br/>10 files, 267 tests, swebench coverage]
75+
UT[pytest tests/unit/<br/>11 files, 315 tests, swebench + setup coverage]
7676
end
7777
7878
subgraph "Layer 5: CI"
@@ -99,7 +99,7 @@ graph TB
9999

100100
### test_hook.sh (Self-Test Suite)
101101

102-
- **Location**: `.claude/hooks/test_hook.sh` (~2,012 lines)
102+
- **Location**: `.claude/hooks/test_hook.sh` (~2,089 lines)
103103
- **Invocation**: `bash .claude/hooks/test_hook.sh --self-test`
104104
- **Test count**: 113 pass / 0 fail
105105
- **Implementation**: `run_self_test()` orchestrates all tests
@@ -301,10 +301,12 @@ graph TB
301301

302302
## Risks, Tech Debt, Open Questions
303303

304-
- **Pytest tests**: `tests/unit/` (10 files, 267 tests) +
305-
`tests/integration/` (7 files, 30 tests) = 297 total;
304+
- **Pytest tests**: `tests/unit/` (11 files, 315 tests) +
305+
`tests/integration/` (7 files, 30 tests) = 345 total;
306306
see [02-benchmark-swebench.md](02-benchmark-swebench.md)
307-
for per-file breakdown; hook testing remains shell-based
307+
for swebench per-file breakdown; `test_setup_wizard.py`
308+
(48 tests) covers the interactive setup wizard;
309+
hook testing remains shell-based
308310
- **Stress test size**: `run_stress_tests.sh` at 2,347 lines
309311
is complex; per-category modules would help
310312
- **Agent tests require TeamCreate**: 103 integration tests

docs/psf/02-benchmark-swebench.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
- **Purpose**: Paired A/B benchmark measuring Plankton hook
66
impact on SWE-bench task resolution via Claude Code
77
- **Scope**: `benchmark/swebench/` (8 Python modules + CLI),
8-
`tests/unit/` (10 files, 267 tests),
8+
`tests/unit/` (11 files, 315 tests — 10 swebench + 1 setup wizard),
99
`tests/integration/` (7 files, 30 tests)
1010
- **Key responsibilities**:
1111
- Drive Claude CLI to solve SWE-bench tasks under two
@@ -280,13 +280,14 @@ graph TD
280280

281281
## Test Coverage
282282

283-
### Unit Tests (267 tests, 10 files)
283+
### Unit Tests (315 tests, 11 files)
284284

285285
| File | Tests | Coverage |
286286
| --- | --- | --- |
287287
| `test_swebench_prereqs.py` | 53 | 15 checks, run_all, format, version |
288288
| `test_swebench_runner.py` | 49 | flip, reset, hooks, abort, resume, dry_run |
289289
| `test_swebench_agent.py` | 47 | cmd, solve, parse, patch, cost, dry_run |
290+
| `test_setup_wizard.py` | 48 | rerunnable config editor, dependency detection |
290291
| `test_swebench_gate.py` | 33 | 6 criteria, run_gate, format, dry_run |
291292
| `test_hal_adapter.py` | 26 | dispatch, hooks, errors, metadata, validation |
292293
| `test_swebench_tasks.py` | 21 | JSONL/HF loading, select, checkout, prepare |
@@ -320,7 +321,7 @@ graph TD
320321
- **Gate run**: `python -m benchmark.swebench gate --repos-dir DIR`
321322
- **Full run**: `python -m benchmark.swebench run --repos-dir DIR`
322323
- **Resume**: `python -m benchmark.swebench run --repos-dir DIR --resume`
323-
- **Tests**: `.venv/bin/python -m pytest tests -x -v` (297 tests)
324+
- **Tests**: `.venv/bin/python -m pytest tests -x -v` (345 tests)
324325
- **HAL harness**: `hal-eval` with `hal_adapter.run` entry point
325326

326327
## Archive

scripts/setup.py

Lines changed: 103 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,9 @@ def select_sections(has_existing_config: bool) -> dict[str, bool]:
414414
"subprocess": not has_existing_config,
415415
}
416416

417+
if has_existing_config and not Confirm.ask("Make targeted edits to specific sections?", default=False):
418+
return {key: False for key in defaults}
419+
417420
prompts = [
418421
("languages", "Edit language enforcement settings?"),
419422
("phases", "Edit phases (auto_format, subprocess_delegation)?"),
@@ -456,6 +459,38 @@ def _ensure_local_bin_on_path(show_hint: bool = False) -> bool:
456459
return True
457460

458461

462+
def _ensure_pip_user_bin_on_path() -> bool:
463+
"""Ensure macOS pip --user script dirs are on PATH.
464+
465+
pip --user installs scripts to ~/Library/Python/<version>/bin on macOS
466+
(system Python), unlike ~/.local/bin on Linux. Glob all version dirs.
467+
"""
468+
macos_pip_base = Path.home() / "Library" / "Python"
469+
if not macos_pip_base.exists():
470+
return False
471+
472+
def _version_key(p: Path) -> tuple[int, ...]:
473+
try:
474+
return tuple(int(x) for x in p.parent.name.split("."))
475+
except ValueError:
476+
return (0,)
477+
478+
# Sort descending so highest version (e.g. 3.13) gets highest PATH priority
479+
all_bin_dirs = sorted(macos_pip_base.glob("*/bin"), key=_version_key, reverse=True)
480+
if not all_bin_dirs:
481+
return False
482+
483+
bin_dir_strs = {str(d) for d in all_bin_dirs}
484+
current_path = os.environ.get("PATH", "")
485+
remaining = [e for e in current_path.split(os.pathsep) if e not in bin_dir_strs]
486+
new_path = os.pathsep.join([str(d) for d in all_bin_dirs] + remaining)
487+
488+
if current_path == new_path:
489+
return False
490+
os.environ["PATH"] = new_path
491+
return True
492+
493+
459494
def _detect_linux_package_manager() -> str | None:
460495
for manager in ("apt-get", "dnf", "yum", "pacman", "apk", "zypper"):
461496
if shutil.which(manager):
@@ -619,6 +654,33 @@ def _install_jaq() -> bool: # noqa: PLR0911
619654
return False
620655

621656

657+
def _install_pre_commit() -> bool:
658+
"""Attempt to install pre-commit using available package tooling."""
659+
install_commands: list[tuple[list[str], str]] = []
660+
if shutil.which("uv"):
661+
install_commands.append((["uv", "tool", "install", "pre-commit"], "Installing pre-commit via uv tool"))
662+
if shutil.which("pipx"):
663+
install_commands.append((["pipx", "install", "pre-commit"], "Installing pre-commit via pipx"))
664+
665+
# pip --user is acceptable here: the setup wizard bootstraps tooling
666+
# before hooks are active, so enforce_package_managers won't block it.
667+
python_cmd = shutil.which("python3") or shutil.which("python")
668+
if python_cmd:
669+
install_commands.append(
670+
([python_cmd, "-m", "pip", "install", "--user", "pre-commit"], "Installing pre-commit via pip --user")
671+
)
672+
673+
for command, description in install_commands:
674+
if not _run_install_command(command, description):
675+
continue
676+
_ensure_local_bin_on_path(show_hint=True)
677+
if "--user" in command:
678+
_ensure_pip_user_bin_on_path()
679+
if shutil.which("pre-commit"):
680+
return True
681+
return False
682+
683+
622684
def _guided_install_missing_tools(missing_required: list[str]) -> list[str]:
623685
if not missing_required:
624686
return []
@@ -662,14 +724,19 @@ def check_tools():
662724
console.print("[bold blue]Checking System Dependencies...[/bold blue]")
663725
_ensure_local_bin_on_path()
664726
missing_required = []
727+
required_tools = list(REQUIRED_TOOLS.keys())
728+
console.print(f" Required tools: {', '.join(required_tools)}")
729+
found_required = 0
665730

666731
for tool, desc in REQUIRED_TOOLS.items():
667732
path = shutil.which(tool)
668733
if path:
669734
console.print(f" [green]✓[/green] {tool} found at {path}")
735+
found_required += 1
670736
else:
671737
console.print(f" [red]✗[/red] {tool} NOT found. {desc}")
672738
missing_required.append(tool)
739+
console.print(f" Required tool status: {found_required}/{len(required_tools)} present")
673740

674741
if missing_required:
675742
missing_required = _guided_install_missing_tools(missing_required)
@@ -953,6 +1020,40 @@ def configure_selected_sections(
9531020
return generated
9541021

9551022

1023+
def _install_pre_commit_hooks() -> None:
1024+
console.print(" Installing pre-commit hooks...")
1025+
try:
1026+
subprocess.run(["pre-commit", "install"], check=True, capture_output=True) # noqa: S607 # nosec B603 B607
1027+
console.print(" [green]✓[/green] pre-commit hooks installed")
1028+
except FileNotFoundError:
1029+
console.print(" [red]✗[/red] pre-commit install failed: binary not found in PATH")
1030+
except subprocess.CalledProcessError as exc:
1031+
detail = (exc.stderr or b"").decode("utf-8", errors="replace").strip()
1032+
console.print(f" [red]✗[/red] pre-commit install failed{': ' + detail if detail else ''}")
1033+
1034+
1035+
def _ensure_pre_commit_ready() -> None:
1036+
if not Path(".pre-commit-config.yaml").exists():
1037+
return
1038+
1039+
if shutil.which("pre-commit"):
1040+
_install_pre_commit_hooks()
1041+
return
1042+
1043+
console.print(" [yellow]![/yellow] .pre-commit-config.yaml found but 'pre-commit' not installed.")
1044+
if not Confirm.ask("Install pre-commit now?", default=True):
1045+
console.print(" [yellow]![/yellow] Skipping pre-commit installation.")
1046+
return
1047+
1048+
if not _install_pre_commit():
1049+
console.print(" [red]✗[/red] Could not install pre-commit automatically.")
1050+
console.print(" [yellow]Manual:[/yellow] uv tool install pre-commit")
1051+
return
1052+
1053+
console.print(" [green]✓[/green] pre-commit installed")
1054+
_install_pre_commit_hooks()
1055+
1056+
9561057
def setup_hooks():
9571058
"""Ensure hooks directory exists and scripts are executable."""
9581059
console.print("\n[bold blue]Setting up Hooks...[/bold blue]")
@@ -971,17 +1072,7 @@ def setup_hooks():
9711072
os.chmod(script, 0o755) # noqa: S103 # nosec B103
9721073
console.print(f" [green]✓[/green] chmod +x {script.name}")
9731074

974-
# Check pre-commit
975-
if Path(".pre-commit-config.yaml").exists():
976-
if shutil.which("pre-commit"):
977-
console.print(" Installing pre-commit hooks...")
978-
try:
979-
subprocess.run(["pre-commit", "install"], check=True) # noqa: S607 # nosec B603 B607
980-
console.print(" [green]✓[/green] pre-commit installed")
981-
except subprocess.CalledProcessError:
982-
console.print(" [red]✗[/red] pre-commit install failed")
983-
else:
984-
console.print(" [yellow]![/yellow] .pre-commit-config.yaml found but 'pre-commit' not installed.")
1075+
_ensure_pre_commit_ready()
9851076

9861077

9871078
@app.command()
@@ -1005,7 +1096,7 @@ def main():
10051096
new_config: dict[str, Any] | None = None
10061097
if not any(section_selection.values()):
10071098
if existing_config:
1008-
console.print(" [yellow]![/yellow] No sections selected; existing configuration will be kept unchanged.")
1099+
console.print(" Existing configuration will be kept unchanged.")
10091100
else:
10101101
console.print(" [red]✗[/red] No sections selected and no existing config found; aborting.")
10111102
raise typer.Exit(code=1)

0 commit comments

Comments
 (0)