jgiox-goodvibes 1.0.0__tar.gz
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.
- jgiox_goodvibes-1.0.0/.gitignore +6 -0
- jgiox_goodvibes-1.0.0/PKG-INFO +24 -0
- jgiox_goodvibes-1.0.0/README.md +8 -0
- jgiox_goodvibes-1.0.0/hatch_build.py +43 -0
- jgiox_goodvibes-1.0.0/pyproject.toml +39 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/__init__.py +1 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/__main__.py +15 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/commands/__init__.py +0 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/commands/init_cmd.py +65 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/main.py +26 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/steps/__init__.py +0 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/steps/configure_mcp.py +80 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/steps/copy_templates.py +75 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/steps/install_headroom.py +52 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/templates/.claude/skills/cavecrew/README.md +41 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/templates/.claude/skills/cavecrew/SKILL.md +82 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/templates/.claude/skills/caveman/README.md +48 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/templates/.claude/skills/caveman/SKILL.md +78 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/templates/.claude/skills/caveman-commit/SKILL.md +65 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/templates/.claude/skills/caveman-compress/SKILL.md +111 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/templates/.claude/skills/caveman-help/SKILL.md +63 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/templates/.claude/skills/caveman-review/SKILL.md +55 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/templates/.claude/skills/caveman-stats/SKILL.md +10 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/templates/.claude/skills/goodvibes-hygiene/SKILL.md +42 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/templates/.github/ISSUE_TEMPLATE/bug_report.yml +54 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/templates/.github/ISSUE_TEMPLATE/feature_request.yml +41 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/templates/.github/PULL_REQUEST_TEMPLATE.md +13 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/templates/CHANGELOG.md +11 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/templates/CLAUDE.md +111 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/templates/CONTRIBUTING.md +55 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/templates/JOURNAL.md +32 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/templates/SECURITY.md +16 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/templates/docs/onboarding.md +95 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/utils/__init__.py +0 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/utils/detect_python.py +37 -0
- jgiox_goodvibes-1.0.0/src/goodvibes_cli/utils/sentinel_merge.py +76 -0
- jgiox_goodvibes-1.0.0/templates/.claude/skills/cavecrew/README.md +41 -0
- jgiox_goodvibes-1.0.0/templates/.claude/skills/cavecrew/SKILL.md +82 -0
- jgiox_goodvibes-1.0.0/templates/.claude/skills/caveman/README.md +48 -0
- jgiox_goodvibes-1.0.0/templates/.claude/skills/caveman/SKILL.md +78 -0
- jgiox_goodvibes-1.0.0/templates/.claude/skills/caveman-commit/SKILL.md +65 -0
- jgiox_goodvibes-1.0.0/templates/.claude/skills/caveman-compress/SKILL.md +111 -0
- jgiox_goodvibes-1.0.0/templates/.claude/skills/caveman-help/SKILL.md +63 -0
- jgiox_goodvibes-1.0.0/templates/.claude/skills/caveman-review/SKILL.md +55 -0
- jgiox_goodvibes-1.0.0/templates/.claude/skills/caveman-stats/SKILL.md +10 -0
- jgiox_goodvibes-1.0.0/templates/.claude/skills/goodvibes-hygiene/SKILL.md +42 -0
- jgiox_goodvibes-1.0.0/templates/.github/ISSUE_TEMPLATE/bug_report.yml +54 -0
- jgiox_goodvibes-1.0.0/templates/.github/ISSUE_TEMPLATE/feature_request.yml +41 -0
- jgiox_goodvibes-1.0.0/templates/.github/PULL_REQUEST_TEMPLATE.md +13 -0
- jgiox_goodvibes-1.0.0/templates/CHANGELOG.md +11 -0
- jgiox_goodvibes-1.0.0/templates/CLAUDE.md +111 -0
- jgiox_goodvibes-1.0.0/templates/CONTRIBUTING.md +55 -0
- jgiox_goodvibes-1.0.0/templates/JOURNAL.md +32 -0
- jgiox_goodvibes-1.0.0/templates/SECURITY.md +16 -0
- jgiox_goodvibes-1.0.0/templates/docs/onboarding.md +95 -0
- jgiox_goodvibes-1.0.0/tests/__init__.py +0 -0
- jgiox_goodvibes-1.0.0/tests/conftest.py +42 -0
- jgiox_goodvibes-1.0.0/tests/test_configure_mcp.py +168 -0
- jgiox_goodvibes-1.0.0/tests/test_copy_templates.py +90 -0
- jgiox_goodvibes-1.0.0/tests/test_install_headroom.py +199 -0
- jgiox_goodvibes-1.0.0/tests/test_main.py +76 -0
- jgiox_goodvibes-1.0.0/tests/test_sentinel_merge.py +114 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jgiox-goodvibes
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: One-command bootstrap for vibe coding projects
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Keywords: claude,cli,llm,scaffold,starter-kit,vibe-coding
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: rich>=14
|
|
9
|
+
Requires-Dist: typer>=0.15
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest-asyncio>=0.25; extra == 'dev'
|
|
12
|
+
Requires-Dist: pytest-cov>=7; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest-mock>=3; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest>=9; extra == 'dev'
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# goodvibes
|
|
18
|
+
|
|
19
|
+
One-command bootstrap for vibe coding projects.
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
pip install jgiox-goodvibes
|
|
23
|
+
goodvibes init
|
|
24
|
+
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Hatchling build hook — copies templates into the wheel when building from sdist."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import pathlib
|
|
5
|
+
import shutil
|
|
6
|
+
|
|
7
|
+
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CustomBuildHook(BuildHookInterface):
|
|
11
|
+
"""Resolve and inject the templates directory into the wheel build.
|
|
12
|
+
|
|
13
|
+
When building directly from source, ../../templates resolves correctly.
|
|
14
|
+
When building from an sdist, the templates are at <sdist-root>/templates/.
|
|
15
|
+
This hook copies whichever location exists into the wheel's goodvibes_cli/templates/.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def initialize(self, version: str, build_data: dict) -> None:
|
|
19
|
+
root = pathlib.Path(self.root)
|
|
20
|
+
|
|
21
|
+
# Direct source build: root = packages/pip/, templates at ../../templates
|
|
22
|
+
templates_source = root / ".." / ".." / "templates"
|
|
23
|
+
# Sdist build: templates were included at the sdist root level
|
|
24
|
+
templates_sdist = root / "templates"
|
|
25
|
+
|
|
26
|
+
if templates_source.exists():
|
|
27
|
+
src = templates_source.resolve()
|
|
28
|
+
elif templates_sdist.exists():
|
|
29
|
+
src = templates_sdist.resolve()
|
|
30
|
+
else:
|
|
31
|
+
return # no templates to bundle
|
|
32
|
+
|
|
33
|
+
dest = root / "src" / "goodvibes_cli" / "templates"
|
|
34
|
+
if dest.exists():
|
|
35
|
+
shutil.rmtree(dest)
|
|
36
|
+
shutil.copytree(src, dest)
|
|
37
|
+
|
|
38
|
+
# Clean up after wheel build completes (cleanup hook handles this)
|
|
39
|
+
self._templates_dest = dest
|
|
40
|
+
|
|
41
|
+
def finalize(self, version: str, build_data: dict, artifact_path: str) -> None:
|
|
42
|
+
if hasattr(self, "_templates_dest") and self._templates_dest.exists():
|
|
43
|
+
shutil.rmtree(self._templates_dest)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "jgiox-goodvibes"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "One-command bootstrap for vibe coding projects"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = { text = "Apache-2.0" }
|
|
8
|
+
keywords = ["scaffold", "vibe-coding", "claude", "llm", "starter-kit", "cli"]
|
|
9
|
+
dependencies = ["typer>=0.15", "rich>=14"]
|
|
10
|
+
|
|
11
|
+
[project.scripts]
|
|
12
|
+
goodvibes = "goodvibes_cli.main:app"
|
|
13
|
+
|
|
14
|
+
[build-system]
|
|
15
|
+
requires = ["hatchling"]
|
|
16
|
+
build-backend = "hatchling.build"
|
|
17
|
+
|
|
18
|
+
[tool.hatch.build.hooks.custom]
|
|
19
|
+
path = "hatch_build.py"
|
|
20
|
+
|
|
21
|
+
[tool.hatch.build.targets.wheel]
|
|
22
|
+
packages = ["src/goodvibes_cli"]
|
|
23
|
+
|
|
24
|
+
[tool.hatch.build.targets.sdist]
|
|
25
|
+
include = ["src/goodvibes_cli/**", "tests/**", "hatch_build.py"]
|
|
26
|
+
# ponytail: include templates in sdist so hook can find them when building from sdist
|
|
27
|
+
force-include = {"../../templates" = "templates"}
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
dev = [
|
|
31
|
+
"pytest>=9",
|
|
32
|
+
"pytest-mock>=3",
|
|
33
|
+
"pytest-asyncio>=0.25",
|
|
34
|
+
"pytest-cov>=7",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
testpaths = ["tests"]
|
|
39
|
+
addopts = "-x -q"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
if sys.version_info < (3, 10):
|
|
4
|
+
print(
|
|
5
|
+
f"goodvibes requires Python 3.10 or higher. "
|
|
6
|
+
f"You have Python {sys.version_info.major}.{sys.version_info.minor}.",
|
|
7
|
+
file=sys.stderr,
|
|
8
|
+
)
|
|
9
|
+
sys.exit(1)
|
|
10
|
+
|
|
11
|
+
import typer # noqa: E402 — version guard must run before any import
|
|
12
|
+
from goodvibes_cli.main import app # noqa: E402
|
|
13
|
+
|
|
14
|
+
if __name__ == "__main__":
|
|
15
|
+
app()
|
|
File without changes
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""goodvibes init command — port of init.ts."""
|
|
2
|
+
import pathlib
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
|
|
9
|
+
from goodvibes_cli.steps.configure_mcp import configure_mcp
|
|
10
|
+
from goodvibes_cli.steps.copy_templates import copy_templates, list_template_files, resolve_templates_dir
|
|
11
|
+
from goodvibes_cli.steps.install_headroom import install_headroom
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
_NEXT_STEPS = (
|
|
16
|
+
"1. Open this project in Claude Code\n"
|
|
17
|
+
"2. In Claude Code CLI: /plugin marketplace add DietrichGebert/ponytail\n"
|
|
18
|
+
"3. Start coding — CLAUDE.md rules are already active"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def init_cmd(
|
|
23
|
+
dry_run: Annotated[bool, typer.Option("--dry-run", help="Preview files without writing")] = False,
|
|
24
|
+
minimal: Annotated[bool, typer.Option("--minimal", help="Skip headroom install and CI workflows")] = False,
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Bootstrap a project with goodvibes configuration."""
|
|
27
|
+
template_dir = resolve_templates_dir()
|
|
28
|
+
cwd = pathlib.Path.cwd()
|
|
29
|
+
|
|
30
|
+
console.rule("[bold]goodvibes init[/bold]")
|
|
31
|
+
|
|
32
|
+
if dry_run:
|
|
33
|
+
files = list_template_files(template_dir)
|
|
34
|
+
file_list = "\n".join(f" Would write: {f}" for f in files)
|
|
35
|
+
console.print(Panel(file_list, title="Dry run — no files written"))
|
|
36
|
+
console.print(Panel(_NEXT_STEPS, title="Next steps"))
|
|
37
|
+
console.rule("Run without --dry-run to apply these changes.")
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
created_files: list[str] = []
|
|
41
|
+
|
|
42
|
+
with console.status("Copying template files") as status:
|
|
43
|
+
def log_copy(msg: str) -> None:
|
|
44
|
+
status.update(msg)
|
|
45
|
+
|
|
46
|
+
files = copy_templates(template_dir, cwd, dry_run=False, minimal=minimal)
|
|
47
|
+
created_files.extend(files)
|
|
48
|
+
|
|
49
|
+
if not minimal:
|
|
50
|
+
with console.status("Installing headroom") as status:
|
|
51
|
+
def log_install(msg: str) -> None:
|
|
52
|
+
status.update(msg)
|
|
53
|
+
|
|
54
|
+
install_headroom(log_install)
|
|
55
|
+
|
|
56
|
+
with console.status("Configuring headroom MCP") as status:
|
|
57
|
+
def log_mcp(msg: str) -> None:
|
|
58
|
+
status.update(msg)
|
|
59
|
+
|
|
60
|
+
configure_mcp(log_mcp)
|
|
61
|
+
|
|
62
|
+
file_list_str = "\n".join(created_files) if created_files else "(none)"
|
|
63
|
+
console.print(Panel(file_list_str, title="Files created"))
|
|
64
|
+
console.print(Panel(_NEXT_STEPS, title="Next steps"))
|
|
65
|
+
console.rule("[green]You're all set![/green]")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
if sys.version_info < (3, 10):
|
|
4
|
+
print(
|
|
5
|
+
f"goodvibes requires Python 3.10 or higher. "
|
|
6
|
+
f"You have Python {sys.version_info.major}.{sys.version_info.minor}.",
|
|
7
|
+
file=sys.stderr,
|
|
8
|
+
)
|
|
9
|
+
sys.exit(1)
|
|
10
|
+
|
|
11
|
+
import typer # noqa: E402 — version guard must run before any import
|
|
12
|
+
|
|
13
|
+
from goodvibes_cli.commands.init_cmd import init_cmd # noqa: E402
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(help="goodvibes — one-command bootstrap for vibe coding projects")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.callback()
|
|
19
|
+
def _callback() -> None:
|
|
20
|
+
"""goodvibes CLI — run 'goodvibes init' to bootstrap a project"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
app.command("init")(init_cmd)
|
|
24
|
+
|
|
25
|
+
if __name__ == "__main__":
|
|
26
|
+
app()
|
|
File without changes
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Register headroom as a global MCP server in Claude Code."""
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def configure_mcp(log: Callable[[str], None]) -> None:
|
|
8
|
+
"""Register headroom as a global MCP server via claude mcp add (primary) or headroom mcp install (fallback).
|
|
9
|
+
|
|
10
|
+
Strategy:
|
|
11
|
+
1. Idempotency: headroom mcp status (exit 0 → already registered)
|
|
12
|
+
2. Primary: claude mcp add -s user headroom <absolute-path>
|
|
13
|
+
3. Fallback: headroom mcp install (when claude CLI not on PATH)
|
|
14
|
+
|
|
15
|
+
Never writes to ~/.claude/ directly. Never uses shell=True.
|
|
16
|
+
"""
|
|
17
|
+
# Step 1: idempotency check
|
|
18
|
+
try:
|
|
19
|
+
subprocess.run(
|
|
20
|
+
["headroom", "mcp", "status"],
|
|
21
|
+
capture_output=True,
|
|
22
|
+
text=True,
|
|
23
|
+
check=True,
|
|
24
|
+
)
|
|
25
|
+
log("headroom MCP already configured — skipping")
|
|
26
|
+
return
|
|
27
|
+
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
# Step 2: primary — claude mcp add -s user (handles CLAUDE_CONFIG_DIR correctly)
|
|
31
|
+
try:
|
|
32
|
+
list_result = subprocess.run(
|
|
33
|
+
["claude", "mcp", "list"],
|
|
34
|
+
capture_output=True,
|
|
35
|
+
text=True,
|
|
36
|
+
check=True,
|
|
37
|
+
)
|
|
38
|
+
if "headroom" in list_result.stdout:
|
|
39
|
+
log("headroom already registered in claude MCP — skipping")
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
absolute_path = shutil.which("headroom")
|
|
43
|
+
if not absolute_path:
|
|
44
|
+
log(
|
|
45
|
+
"headroom binary not found on PATH — MCP registration skipped. "
|
|
46
|
+
'Run `uv tool install "headroom-ai[all]"` then re-run `goodvibes init`.'
|
|
47
|
+
)
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
subprocess.run(
|
|
51
|
+
["claude", "mcp", "add", "-s", "user", "headroom", absolute_path],
|
|
52
|
+
capture_output=True,
|
|
53
|
+
text=True,
|
|
54
|
+
check=True,
|
|
55
|
+
)
|
|
56
|
+
log("headroom registered as global MCP server")
|
|
57
|
+
return
|
|
58
|
+
except FileNotFoundError:
|
|
59
|
+
# claude CLI not on PATH — fall back to headroom mcp install
|
|
60
|
+
log("claude CLI not found — falling back to headroom mcp install")
|
|
61
|
+
log(
|
|
62
|
+
"Warning: if you use CLAUDE_CONFIG_DIR, you may need to run "
|
|
63
|
+
"`headroom mcp install` manually"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Step 3: fallback — headroom mcp install
|
|
67
|
+
try:
|
|
68
|
+
subprocess.run(
|
|
69
|
+
["headroom", "mcp", "install"],
|
|
70
|
+
capture_output=True,
|
|
71
|
+
text=True,
|
|
72
|
+
check=True,
|
|
73
|
+
)
|
|
74
|
+
except FileNotFoundError:
|
|
75
|
+
log(
|
|
76
|
+
"headroom binary not found — MCP registration skipped. "
|
|
77
|
+
"Install headroom and run `headroom mcp install` manually."
|
|
78
|
+
)
|
|
79
|
+
except subprocess.CalledProcessError:
|
|
80
|
+
raise # unexpected failure in fallback — re-raise so caller can surface it
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""File copy orchestrator — port of copy-templates.ts. Uses shutil + importlib.resources."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import importlib.resources
|
|
5
|
+
import pathlib
|
|
6
|
+
import shutil
|
|
7
|
+
|
|
8
|
+
from goodvibes_cli.utils.sentinel_merge import merge_claude
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def resolve_templates_dir() -> pathlib.Path:
|
|
12
|
+
"""Return the bundled templates directory from the installed wheel."""
|
|
13
|
+
ref = importlib.resources.files("goodvibes_cli").joinpath("templates")
|
|
14
|
+
# Wrap with Path(str(...)) for str/PathLike compatibility (RESEARCH.md Pitfall 2)
|
|
15
|
+
path = pathlib.Path(str(ref))
|
|
16
|
+
if not path.exists():
|
|
17
|
+
raise FileNotFoundError("goodvibes template files not found in installed package")
|
|
18
|
+
return path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def list_template_files(template_dir: pathlib.Path) -> list[str]:
|
|
22
|
+
"""Return sorted list of relative file paths under template_dir."""
|
|
23
|
+
return sorted(
|
|
24
|
+
str(f.relative_to(template_dir))
|
|
25
|
+
for f in template_dir.rglob("*")
|
|
26
|
+
if f.is_file()
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def copy_templates(
|
|
31
|
+
template_dir: pathlib.Path,
|
|
32
|
+
dest_dir: pathlib.Path,
|
|
33
|
+
dry_run: bool = False,
|
|
34
|
+
minimal: bool = False,
|
|
35
|
+
) -> list[str]:
|
|
36
|
+
"""Copy template files to dest_dir, handling CLAUDE.md via sentinel merge.
|
|
37
|
+
|
|
38
|
+
Returns the list of template file paths (relative to template_dir).
|
|
39
|
+
When dry_run=True, returns the list without writing any files.
|
|
40
|
+
"""
|
|
41
|
+
if dry_run:
|
|
42
|
+
return list_template_files(template_dir)
|
|
43
|
+
|
|
44
|
+
def ignore_fn(directory: str, contents: list[str]) -> set[str]:
|
|
45
|
+
ignored: set[str] = set()
|
|
46
|
+
for name in contents:
|
|
47
|
+
full = pathlib.Path(directory) / name
|
|
48
|
+
try:
|
|
49
|
+
rel = full.relative_to(template_dir)
|
|
50
|
+
except ValueError:
|
|
51
|
+
# path not relative to template_dir — path traversal
|
|
52
|
+
ignored.add(name)
|
|
53
|
+
continue
|
|
54
|
+
if name == "CLAUDE.md":
|
|
55
|
+
ignored.add(name) # sentinel merge handles it separately
|
|
56
|
+
if ".." in pathlib.Path(str(rel)).parts:
|
|
57
|
+
ignored.add(name) # path traversal guard (T-03-02-01)
|
|
58
|
+
if minimal and ".github" in rel.parts and "workflows" in rel.parts:
|
|
59
|
+
ignored.add(name)
|
|
60
|
+
# No-clobber: skip if destination already exists (T-03-02-03)
|
|
61
|
+
dest_candidate = dest_dir / rel
|
|
62
|
+
if dest_candidate.exists():
|
|
63
|
+
ignored.add(name)
|
|
64
|
+
return ignored
|
|
65
|
+
|
|
66
|
+
shutil.copytree(str(template_dir), str(dest_dir), ignore=ignore_fn, dirs_exist_ok=True)
|
|
67
|
+
|
|
68
|
+
# Handle CLAUDE.md via sentinel merge
|
|
69
|
+
claude_src = template_dir / "CLAUDE.md"
|
|
70
|
+
if claude_src.exists():
|
|
71
|
+
claude_dest = dest_dir / "CLAUDE.md"
|
|
72
|
+
template_content = claude_src.read_text(encoding="utf-8")
|
|
73
|
+
merge_claude(claude_dest, template_content)
|
|
74
|
+
|
|
75
|
+
return list_template_files(template_dir)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Install headroom-ai via uv → pipx → pip fallback chain."""
|
|
2
|
+
import subprocess
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
from goodvibes_cli.utils.detect_python import detect_python
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def install_headroom(log: Callable[[str], None]) -> None:
|
|
9
|
+
"""Install headroom-ai[all] using the first available installer.
|
|
10
|
+
|
|
11
|
+
Order: uv tool install → pipx install → pip install --user.
|
|
12
|
+
|
|
13
|
+
Never raises. Never uses shell=True. Soft-fails on CalledProcessError.
|
|
14
|
+
"""
|
|
15
|
+
python_cmd = detect_python()
|
|
16
|
+
if python_cmd is None:
|
|
17
|
+
log(
|
|
18
|
+
"Python 3.10+ not found — skipping headroom install. "
|
|
19
|
+
"Install Python 3.10+ and run `goodvibes init` again."
|
|
20
|
+
)
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
# HDR-03: ONNX warning BEFORE any subprocess call — shows even if installer is slow
|
|
24
|
+
log(
|
|
25
|
+
"Note: headroom will download its compression model on first use"
|
|
26
|
+
" — this may take 1–3 minutes on a slow connection."
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
installers: list[list[str]] = [
|
|
30
|
+
["uv", "tool", "install", "headroom-ai[all]"],
|
|
31
|
+
["pipx", "install", "headroom-ai[all]"],
|
|
32
|
+
[python_cmd, "-m", "pip", "install", "--user", "headroom-ai[all]"],
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
for cmd_list in installers:
|
|
36
|
+
try:
|
|
37
|
+
subprocess.run(cmd_list, capture_output=True, text=True, check=True)
|
|
38
|
+
return
|
|
39
|
+
except FileNotFoundError:
|
|
40
|
+
continue
|
|
41
|
+
except subprocess.CalledProcessError as e:
|
|
42
|
+
lines = (e.stderr or "").splitlines()
|
|
43
|
+
first_line = lines[0] if lines else "unknown error"
|
|
44
|
+
log(f"headroom install failed: {first_line}")
|
|
45
|
+
log('You can install headroom manually later: uv tool install "headroom-ai[all]"')
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
# All three installers were FileNotFoundError
|
|
49
|
+
log(
|
|
50
|
+
"No package installer found (uv, pipx, pip). "
|
|
51
|
+
"Install Python 3.10+ with uv or pipx and run `goodvibes init` again."
|
|
52
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# cavecrew
|
|
2
|
+
|
|
3
|
+
Decision guide. When to delegate to caveman subagents instead of doing the work inline.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
Tells the main thread when to spawn a caveman-style subagent versus the vanilla equivalent. The win: subagent tool-results inject back into main context verbatim, and caveman output is roughly 1/3 the size of vanilla prose. Across 20 delegations in one session, that is the difference between context exhaustion and finishing the task.
|
|
8
|
+
|
|
9
|
+
Three subagents:
|
|
10
|
+
|
|
11
|
+
| Subagent | Job | Use when |
|
|
12
|
+
|----------|-----|----------|
|
|
13
|
+
| `cavecrew-investigator` | Locate code (read-only) | "Where is X defined / what calls Y / list uses of Z" |
|
|
14
|
+
| `cavecrew-builder` | Surgical edit, 1-2 files | Scope is obvious, ≤2 files. Refuses 3+ file scope. |
|
|
15
|
+
| `cavecrew-reviewer` | Diff/file review | One-line findings with severity emoji |
|
|
16
|
+
|
|
17
|
+
Use vanilla `Explore` or `Code Reviewer` when you want prose, architecture commentary, or rationale. Use main thread directly for one-line answers and 3+ file refactors.
|
|
18
|
+
|
|
19
|
+
This skill is a decision guide, not a slash command. It activates when the conversation mentions delegation.
|
|
20
|
+
|
|
21
|
+
## How to invoke
|
|
22
|
+
|
|
23
|
+
Triggers on phrases like "delegate to subagent", "use cavecrew", "spawn investigator", "save context", "compressed agent output".
|
|
24
|
+
|
|
25
|
+
## Example chaining
|
|
26
|
+
|
|
27
|
+
Locate → fix → verify (most common):
|
|
28
|
+
|
|
29
|
+
1. `cavecrew-investigator` returns site list (`path:line — symbol — note`)
|
|
30
|
+
2. Main thread picks 1-2 sites, hands paths to `cavecrew-builder`
|
|
31
|
+
3. `cavecrew-reviewer` audits the resulting diff
|
|
32
|
+
|
|
33
|
+
Parallel scout: spawn 2-3 `cavecrew-investigator` calls in one message with different angles (defs, callers, tests). Aggregate in main.
|
|
34
|
+
|
|
35
|
+
## See also
|
|
36
|
+
|
|
37
|
+
- [`SKILL.md`](./SKILL.md) — full decision matrix and output contracts
|
|
38
|
+
- [`agents/cavecrew-investigator.md`](../../agents/cavecrew-investigator.md)
|
|
39
|
+
- [`agents/cavecrew-builder.md`](../../agents/cavecrew-builder.md)
|
|
40
|
+
- [`agents/cavecrew-reviewer.md`](../../agents/cavecrew-reviewer.md)
|
|
41
|
+
- [Caveman README](../../README.md) — repo overview
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cavecrew
|
|
3
|
+
description: >
|
|
4
|
+
Decision guide for delegating to caveman-style subagents. Tells the main
|
|
5
|
+
thread WHEN to spawn `cavecrew-investigator` (locate code), `cavecrew-builder`
|
|
6
|
+
(1-2 file edit), or `cavecrew-reviewer` (diff review) instead of doing the
|
|
7
|
+
work inline or using vanilla `Explore`. Subagent output is caveman-compressed
|
|
8
|
+
so the tool-result injected back into main context is ~60% smaller — main
|
|
9
|
+
context lasts longer across long sessions.
|
|
10
|
+
Trigger: "delegate to subagent", "use cavecrew", "spawn investigator/builder/reviewer",
|
|
11
|
+
"save context", "compressed agent output".
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
Cavecrew = three subagent presets that emit caveman output. Same job as Anthropic defaults (`Explore`, edit-style agents, reviewer); difference is the tool-result they return is compressed, so main context shrinks per delegation.
|
|
15
|
+
|
|
16
|
+
## When to use cavecrew vs alternatives
|
|
17
|
+
|
|
18
|
+
| Task | Use |
|
|
19
|
+
|---|---|
|
|
20
|
+
| "Where is X defined / what calls Y / list uses of Z" | `cavecrew-investigator` |
|
|
21
|
+
| Same but you also want suggestions/architecture commentary | `Explore` (vanilla) |
|
|
22
|
+
| Surgical edit, ≤2 files, scope obvious | `cavecrew-builder` |
|
|
23
|
+
| New feature / 3+ files / cross-cutting refactor | Main thread or `feature-dev:code-architect` |
|
|
24
|
+
| Review diff, branch, or file for bugs | `cavecrew-reviewer` |
|
|
25
|
+
| Deep code review with rationale + alternatives | `Code Reviewer` (vanilla) |
|
|
26
|
+
| One-line answer you already know | Main thread, no subagent |
|
|
27
|
+
|
|
28
|
+
Rule of thumb: **if you'd want the subagent's output in 1/3 the tokens, pick cavecrew. If you'd want prose, pick vanilla.**
|
|
29
|
+
|
|
30
|
+
## Why this exists (the real win)
|
|
31
|
+
|
|
32
|
+
Subagent tool results get injected into main context verbatim. A vanilla `Explore` that returns 2k tokens of prose costs 2k tokens of main-context budget every time. The same finding from `cavecrew-investigator` returns ~700 tokens. Across 20 delegations in one session that's the difference between context exhaustion and finishing the task.
|
|
33
|
+
|
|
34
|
+
## Output contracts
|
|
35
|
+
|
|
36
|
+
What main thread can rely on per agent:
|
|
37
|
+
|
|
38
|
+
**`cavecrew-investigator`**
|
|
39
|
+
```
|
|
40
|
+
<Header>:
|
|
41
|
+
- path:line — `symbol` — short note
|
|
42
|
+
totals: <counts>.
|
|
43
|
+
```
|
|
44
|
+
Or `No match.` Always file-path-first, line-number-attached, backticked symbols. Safe to grep with `path:\d+`.
|
|
45
|
+
|
|
46
|
+
**`cavecrew-builder`**
|
|
47
|
+
```
|
|
48
|
+
<path:line-range> — <change ≤10 words>.
|
|
49
|
+
verified: <re-read OK | mismatch @ path:line>.
|
|
50
|
+
```
|
|
51
|
+
Or one of: `too-big.` / `needs-confirm.` / `ambiguous.` / `regressed.` (terminal first token).
|
|
52
|
+
|
|
53
|
+
**`cavecrew-reviewer`**
|
|
54
|
+
```
|
|
55
|
+
path:line: <emoji> <severity>: <problem>. <fix>.
|
|
56
|
+
totals: N🔴 N🟡 N🔵 N❓
|
|
57
|
+
```
|
|
58
|
+
Or `No issues.` Findings sorted file → line ascending.
|
|
59
|
+
|
|
60
|
+
## Chaining patterns
|
|
61
|
+
|
|
62
|
+
**Locate → fix → verify** (most common):
|
|
63
|
+
1. `cavecrew-investigator` returns site list.
|
|
64
|
+
2. Main thread picks 1-2 sites, hands paths to `cavecrew-builder`.
|
|
65
|
+
3. `cavecrew-reviewer` audits the diff.
|
|
66
|
+
|
|
67
|
+
**Parallel scout** (when investigation is broad):
|
|
68
|
+
Spawn 2-3 `cavecrew-investigator` calls in one message (different angles: defs vs callers vs tests). Aggregate in main thread.
|
|
69
|
+
|
|
70
|
+
**Single-shot edit** (when site is already known):
|
|
71
|
+
Skip investigator. Hand exact path:line to `cavecrew-builder` directly.
|
|
72
|
+
|
|
73
|
+
## What NOT to do
|
|
74
|
+
|
|
75
|
+
- Don't use `cavecrew-builder` when you don't already know the file. Spawn investigator first or main thread will eat tokens passing context.
|
|
76
|
+
- Don't chain `cavecrew-investigator → cavecrew-builder` for a 5-file refactor. Builder will return `too-big.` and you'll have wasted a turn.
|
|
77
|
+
- Don't ask `cavecrew-reviewer` for "general feedback" — it returns findings only, no architecture opinions. Use `Code Reviewer` for that.
|
|
78
|
+
- Don't expect prose. Cavecrew output is structured, sometimes terse to the point of cryptic. If a human will read it directly, paraphrase.
|
|
79
|
+
|
|
80
|
+
## Auto-clarity (inherited)
|
|
81
|
+
|
|
82
|
+
Subagents drop caveman → normal English for security warnings, irreversible-action confirmations, and any output where fragment ambiguity could be misread. Resume caveman after.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# caveman
|
|
2
|
+
|
|
3
|
+
Talk like smart caveman. Same brain, fewer tokens.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
Compress every model response to caveman-style prose. Drops articles, filler, pleasantries, and hedging. Keeps every technical detail, code block, error string, and symbol exact. Cuts ~65-75% of output tokens with full accuracy preserved. Mode persists for the whole session until changed or stopped.
|
|
8
|
+
|
|
9
|
+
Six intensity levels:
|
|
10
|
+
|
|
11
|
+
| Level | What change |
|
|
12
|
+
|-------|-------------|
|
|
13
|
+
| `lite` | Drop filler/hedging. Sentences stay full. Professional but tight. |
|
|
14
|
+
| `full` | Default. Drop articles, fragments OK, short synonyms. |
|
|
15
|
+
| `ultra` | Bare fragments. Abbreviations (DB, auth, fn). Arrows for causality. |
|
|
16
|
+
| `wenyan-lite` | Classical Chinese register, light compression. |
|
|
17
|
+
| `wenyan-full` | Maximum 文言文. 80-90% character reduction. |
|
|
18
|
+
| `wenyan-ultra` | Extreme classical compression. |
|
|
19
|
+
|
|
20
|
+
Auto-clarity rule: caveman drops to normal prose for security warnings, irreversible-action confirmations, multi-step sequences where fragment ambiguity risks misread, and when user repeats a question. Resumes after the clear part.
|
|
21
|
+
|
|
22
|
+
## How to invoke
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
/caveman # full mode (default)
|
|
26
|
+
/caveman lite # lighter compression
|
|
27
|
+
/caveman ultra # extreme compression
|
|
28
|
+
/caveman wenyan # classical Chinese
|
|
29
|
+
stop caveman # back to normal prose
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Example output
|
|
33
|
+
|
|
34
|
+
Question: "Why does my React component re-render?"
|
|
35
|
+
|
|
36
|
+
Normal prose:
|
|
37
|
+
> Your component re-renders because you create a new object reference each render. Wrapping it in `useMemo` will fix the issue.
|
|
38
|
+
|
|
39
|
+
Caveman (full):
|
|
40
|
+
> New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`.
|
|
41
|
+
|
|
42
|
+
Caveman (ultra):
|
|
43
|
+
> Inline obj prop → new ref → re-render. `useMemo`.
|
|
44
|
+
|
|
45
|
+
## See also
|
|
46
|
+
|
|
47
|
+
- [`SKILL.md`](./SKILL.md) — full LLM-facing instructions
|
|
48
|
+
- [Caveman README](../../README.md) — repo overview, install, benchmarks
|