Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: CI

on:
pull_request:
push:
branches:
- main

jobs:
verify:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x"

- name: Run tests
run: python3 -m unittest discover -s tests -p 'test_*.py'

- name: Audit repository-generated agent stack
run: python3 scripts/audit_agent_stack.py --repo-only

- name: Inventory optional tools without installing
run: python3 scripts/inventory_optional_tools.py --json

- name: Scan for private paths and secret assignments
run: python3 scripts/check_private_paths.py

- name: Check whitespace
run: git diff --check
9 changes: 9 additions & 0 deletions docs/agent-setup-playbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ When two valid scopes could apply, prefer the smaller one and explain what is in

Inventory tools before installing anything.

This repository includes a read-only optional tool inventory helper. It does not install tools.

```bash
python3 scripts/inventory_optional_tools.py
python3 scripts/inventory_optional_tools.py --json
```

Use its output as evidence, not as permission to install. The agent must still classify each tool, explain the reason, and ask before installing anything.

For every relevant tool, report:

- detected state: installed, missing, unknown, or not applicable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -831,7 +831,7 @@ python3 scripts/render_claude_plugin.py --partner-name "Hun"
Expected:

```text
Rendered Claude plugin bundle at /Users/hooooonje/codex-dotfiles/plugins/process-first-agents
Rendered Claude plugin bundle at <repo>/plugins/process-first-agents
```

- [ ] **Step 6: Run focused tests**
Expand Down Expand Up @@ -1032,7 +1032,7 @@ python3 scripts/render_claude_plugin.py --partner-name "Hun"
Expected:

```text
Rendered Claude plugin bundle at /Users/hooooonje/codex-dotfiles/plugins/process-first-agents
Rendered Claude plugin bundle at <repo>/plugins/process-first-agents
```

- [ ] **Step 5: Run focused tests**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ class VibeCodingGuardrailsDocsTests(unittest.TestCase):
for heading in expected_headings:
self.assertIn(heading, template)

self.assertNotIn("/Users/", template)
self.assertNotIn("/" + "Users/", template)
self.assertNotIn("C:\\\\Users\\\\", template)

def test_copy_paste_prompts_cover_apply_and_start_workflows(self) -> None:
Expand Down
23 changes: 23 additions & 0 deletions docs/vibe-coding-guardrails.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,29 @@ The fix is not one huge prompt. The fix is a repeatable operating loop:

The loop should be boring. Boring is good here.

## Applied Checklist Mapping

This section maps the original vibe-coding problem checklist to concrete artifacts in this repository.

| Original issue or fix | Repository application |
| --- | --- |
| agent memory is not continuous | `docs/agent-setup-playbook.md` and `docs/local-project-knowledge-template.md` tell agents to build persistent project maps instead of rediscovering helpers, types, shapes, commands, and boundaries every session. |
| hidden coupling | `AGENTS.md`, this guide, and role prompts require pre-write checks for module boundaries, dependency direction, hidden imports, initialization order, global state, and side effects. |
| weak tests | `AGENTS.md` requires TDD; this guide requires behavior, failure-path, edge-case, and side-effect tests instead of string-only checks. |
| edge cases | The write gate requires empty input, null or missing values, boundary values, failure paths, side effects, and concurrency checks when relevant. |
| deep nesting | `AGENTS.md` tells agents to use guard clauses or early returns when nesting grows past two or three levels. |
| god functions | This guide tells agents to avoid god functions and god files, and the post-write review checks whether responsibility concentrated in one file. |
| circular dependencies | The pre-write lens asks agents to inspect dependency direction and likely cycle risks before editing. |
| re-export drift | The pre-write lens and post-write review both call out re-export and barrel files as coupling risks. |
| silent fallback | `AGENTS.md`, this guide, and the setup playbook all forbid swallowed errors and fallback behavior unless it is a documented requirement. |
| flat directories | `docs/local-project-knowledge-template.md` asks agents to record architecture maps, module boundaries, dependency direction, public APIs, and known hotspots so directory boundaries stay visible. |

The support scripts reinforce those rules:

- `scripts/inventory_optional_tools.py` gives agents a read-only way to classify Obsidian, Lumin Repo Lens, dependency lint, strict type checks, cycle detection, and complexity limits before asking to install anything.
- `scripts/check_private_paths.py` scans tracked files for concrete private paths and secret assignments.
- `scripts/audit_agent_stack.py` checks local harness state and generated Claude plugin drift; `--repo-only` keeps CI focused on repository artifacts.

## Operating Loop

### Pre-write lens
Expand Down
11 changes: 11 additions & 0 deletions scripts/audit_agent_stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,11 @@ def run_audit(
superpowers_path: Path,
agents_home: Path,
online: bool,
repo_only: bool = False,
) -> list[CheckResult]:
if repo_only:
return [check_claude_generated_bundle(repo_root)]

checks = [
check_cli(name, command, package_name, required, online)
for name, command, package_name, required in CLI_CHECKS
Expand Down Expand Up @@ -430,6 +434,11 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
action="store_true",
help="Treat missing optional tools as failures.",
)
parser.add_argument(
"--repo-only",
action="store_true",
help="Skip local CLI and user-home checks; verify repository-generated artifacts only.",
)
return parser.parse_args(argv)


Expand All @@ -445,13 +454,15 @@ def main(argv: list[str]) -> int:
Path(args.superpowers_path),
Path(args.agents_home),
args.online,
args.repo_only,
)
failed = should_fail(checks, args.strict)

if args.json:
payload = {
"ok": not failed,
"online": args.online,
"repo_only": args.repo_only,
"checks": [asdict(check) for check in checks],
}
print(json.dumps(payload, indent=2, sort_keys=True))
Expand Down
126 changes: 126 additions & 0 deletions scripts/check_private_paths.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#!/usr/bin/env python3
"""Scan tracked text files for concrete private paths and secret assignments."""

from __future__ import annotations

import argparse
import re
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path


MAC_USER_PREFIX = "/" + "Users/"
WINDOWS_USER_PREFIX = "C:" + "\\Users\\"

PRIVATE_PATTERNS = (
(
"private-user-path",
re.compile(
rf"({re.escape(MAC_USER_PREFIX)}[^\s`'\"<>]+|{re.escape(WINDOWS_USER_PREFIX)}[^\s`'\"<>]+)"
),
),
(
"secret-assignment",
re.compile(
r"(?i)\b[A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|API_KEY|AUTH_COOKIE)\b\s*[:=]\s*['\"]?[^'\"\s]+"
),
),
)
DEFAULT_EXCLUDED_SUFFIXES = {
".pyc",
".png",
".jpg",
".jpeg",
".gif",
".ico",
".pdf",
".zip",
}


@dataclass(frozen=True)
class Finding:
path: str
line: int
label: str
excerpt: str


def tracked_files(repo_root: Path) -> list[Path]:
result = subprocess.run(
["git", "ls-files"],
cwd=repo_root,
capture_output=True,
text=True,
)
if result.returncode != 0:
raise RuntimeError(result.stderr.strip() or "git ls-files failed")
return [repo_root / line for line in result.stdout.splitlines() if line.strip()]


def should_scan(path: Path) -> bool:
if path.suffix.lower() in DEFAULT_EXCLUDED_SUFFIXES:
return False
return True


def scan_text(path: Path, text: str) -> list[Finding]:
findings: list[Finding] = []
for line_number, line in enumerate(text.splitlines(), start=1):
for label, pattern in PRIVATE_PATTERNS:
if pattern.search(line):
findings.append(
Finding(
path=str(path),
line=line_number,
label=label,
excerpt=line.strip()[:160],
)
)
return findings


def scan_paths(paths: list[Path]) -> list[Finding]:
findings: list[Finding] = []
for path in paths:
if not should_scan(path):
continue
try:
text = path.read_text(encoding="utf-8")
except UnicodeDecodeError:
continue
findings.extend(scan_text(path, text))
return findings


def render_findings(findings: list[Finding]) -> str:
if not findings:
return "Private path scan: ok"
lines = ["Private path scan: findings"]
for finding in findings:
lines.append(f"- {finding.path}:{finding.line}: {finding.label}: {finding.excerpt}")
return "\n".join(lines)


def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--repo-root",
default=".",
help="Repository root. Defaults to the current directory.",
)
return parser.parse_args(argv)


def main(argv: list[str]) -> int:
args = parse_args(argv)
repo_root = Path(args.repo_root).expanduser().resolve()
findings = scan_paths(tracked_files(repo_root))
print(render_findings(findings))
return 1 if findings else 0


if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
Loading
Loading