From c61c988966fb69a03e79cba2acef8c52627147d5 Mon Sep 17 00:00:00 2001 From: rob Date: Sat, 25 Oct 2025 02:24:55 -0300 Subject: [PATCH] 1st commit --- Docs/DESIGN.md => DESIGN.md | 0 assets/hooks/pre-commit | 81 ++++++++ src/cascadingdev/setup_project.py | 310 ++++++++++++++++++++++++++++++ 3 files changed, 391 insertions(+) rename Docs/DESIGN.md => DESIGN.md (100%) create mode 100644 assets/hooks/pre-commit create mode 100644 src/cascadingdev/setup_project.py diff --git a/Docs/DESIGN.md b/DESIGN.md similarity index 100% rename from Docs/DESIGN.md rename to DESIGN.md diff --git a/assets/hooks/pre-commit b/assets/hooks/pre-commit new file mode 100644 index 0000000..92356ee --- /dev/null +++ b/assets/hooks/pre-commit @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo ".")" +cd "$ROOT" + +# -------- collect staged files ---------- +mapfile -t STAGED < <(git diff --cached --name-only --diff-filter=AM || true) +[ "${#STAGED[@]}" -eq 0 ] && exit 0 + +# -------- tiny secret scan (fast, regex only) ---------- +DIFF="$(git diff --cached)" +if echo "$DIFF" | grep -Eqi '(api[_-]?key|secret|access[_-]?token|private[_-]?key)[:=]\s*[A-Za-z0-9_\-]{12,}'; then + echo >&2 "[pre-commit] Possible secret detected in staged changes." + echo >&2 " If false positive, commit with --no-verify and add an allowlist later." + exit 11 +fi + +# -------- ensure discussion summaries exist (companion files) ---------- +ensure_summary() { + local disc="$1" + local dir; dir="$(dirname "$disc")" + local sum="$dir/$(basename "$disc" .md).sum.md" + if [ ! -f "$sum" ]; then + cat > "$sum" <<'EOF' +# Summary — + + +## Decisions (ADR-style) +- (none yet) + + + +## Open Questions +- (none yet) + + + +## Awaiting Replies +- (none yet) + + + +## Action Items +- (none yet) + + + +## Votes (latest per participant) +READY: 0 • CHANGES: 0 • REJECT: 0 +- (no votes yet) + + + +## Timeline (most recent first) +- : + + + +## Links +- Related PRs: – +- Commits: – +- Design/Plan: ../design/design.md + +EOF + git add "$sum" + fi +} + +for f in "${STAGED[@]}"; do + case "$f" in + Docs/features/*/discussions/*.discussion.md) ensure_summary "$f";; + esac +done + +# -------- future orchestration (non-blocking status) ---------- +if [ -x "automation/workflow.py" ]; then + python3 automation/workflow.py --status || true +fi + +exit 0 diff --git a/src/cascadingdev/setup_project.py b/src/cascadingdev/setup_project.py new file mode 100644 index 0000000..ace4f81 --- /dev/null +++ b/src/cascadingdev/setup_project.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python3 +""" +setup_project.py — Installer-mode bootstrap for Cascading Development + +Run this from your installation folder (NOT inside the destination repo): +- Prompts (or use --target) for the destination repo path +- Copies essential files from installer → target (DESIGN.md, ramble.py, hooks) +- Creates canonical structure, seeds rules/templates +- Initializes git and installs pre-commit hook +- Launches Ramble to capture the first feature request + +Examples: + python3 scripts/setup_project.py + python3 scripts/setup_project.py --target ~/dev/my-new-repo + python3 scripts/setup_project.py --target /abs/path --no-ramble +""" +import os +import sys +import json +import shutil +import argparse +import subprocess +import datetime +from pathlib import Path + +INSTALL_ROOT = Path(__file__).resolve().parent.parent # installer root (contains this scripts/ dir) + +# ---------- helpers ---------- +def sh(cmd, check=True, cwd=None): + return subprocess.run(cmd, check=check, text=True, capture_output=True, cwd=cwd) + +def say(msg): print(msg, flush=True) + +def write_if_missing(path: Path, content: str): + path.parent.mkdir(parents=True, exist_ok=True) + if not path.exists(): + path.write_text(content, encoding="utf-8") + +def copy_if_exists(src: Path, dst: Path): + if src.exists(): + dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(str(src), str(dst)) + +def ensure_git_repo(target: Path): + if not (target / ".git").exists(): + sh(["git", "init", "-b", "main"], cwd=str(target)) + write_if_missing(target / ".gitignore", "\n".join([ + ".env", ".env.*", "secrets/", ".git/ai-rules-*", "__pycache__/", + "*.pyc", ".pytest_cache/", ".DS_Store", + ]) + "\n") + +def install_precommit_hook(target: Path): + hook_src = INSTALL_ROOT / "scripts" / "hooks" / "pre-commit" + hooks_dir = target / ".git" / "hooks" + hooks_dir.mkdir(parents=True, exist_ok=True) + hook_dst = hooks_dir / "pre-commit" + if not hook_src.exists(): + say("[-] pre-commit hook source missing at scripts/hooks/pre-commit in the installer.") + return + hook_dst.write_text(hook_src.read_text(encoding="utf-8"), encoding="utf-8") + hook_dst.chmod(0o755) + say(f"[+] Installed git hook → {hook_dst}") + +def run_ramble_and_collect(target: Path, provider: str = "mock"): + ramble = target / "ramble.py" + if not ramble.exists(): + say("[-] ramble.py not found in target; skipping interactive FR capture.") + return None + + args = [ + sys.executable, str(ramble), + "--provider", provider, + "--prompt", "Describe your initial feature request for this repository", + "--fields", "Summary", "Title", "Intent", "ProblemItSolves", "BriefOverview", + "--criteria", '{"Summary":"<= 2 sentences","Title":"camelCase, <= 24 chars"}' + ] + say("[•] Launching Ramble (close the dialog to continue)…") + proc = subprocess.run(args, text=True, capture_output=True, cwd=str(target)) + out = (proc.stdout or "").strip() + try: + return json.loads(out) + except Exception: + say("[-] Could not parse Ramble output; proceeding with template text.") + return None + +def seed_process_and_rules(target: Path): + write_if_missing(target / "process" / "design.md", + "# Process & Architecture (Local Notes)\n\n(See DESIGN.md for full spec.)\n") + write_if_missing(target / "process" / "policies.md", + "# Policies (Human-readable)\n\nSee machine-readable config in policies.yml.\n") + write_if_missing(target / "process" / "policies.yml", + """version: 1 +voting: + values: [READY, CHANGES, REJECT] + allow_agent_votes: true + quorum: + discussion: { ready: all, reject: all } + implementation: { ready: 1_human, reject: all } + review: { ready: 1_human, reject: all } +eligibility: + agents_allowed: true + require_human_for: [implementation, review] +etiquette: + name_prefix_agents: "AI_" + vote_line_regex: "^VOTE:\\s*(READY|CHANGES|REJECT)\\s*$" + response_timeout_hours: 24 +""") + + tmpl_dir = target / "process" / "templates" + write_if_missing(tmpl_dir / "feature_request.md", + "# Feature Request: \n\n**Intent**: …\n**Motivation / Problem**: …\n**Constraints / Non-Goals**: …\n**Rough Proposal**: …\n**Open Questions**: …\n") + write_if_missing(tmpl_dir / "discussion.md", + "---\ntype: discussion\nstage: <feature|design|implementation|testing|review>\nstatus: OPEN\ncreated: <YYYY-MM-DD>\n---\n\n## Summary\n\n## Participation\n") + write_if_missing(tmpl_dir / "design_doc.md", + "# Design — <FR id / Title>\n\n## Context & Goals\n## Non-Goals & Constraints\n## Options Considered\n## Decision & Rationale\n## Architecture Diagram(s)\n## Risks & Mitigations\n## Acceptance Criteria\n") + + write_if_missing(target / ".ai-rules.yml", + """version: 1 +file_associations: + "*.md": "md-file" + +rules: + md-file: + description: "Normalize Markdown" + instruction: | + Keep markdown tidy (headings, lists, spacing). No content churn. +settings: + model: "local-mock" + temperature: 0.1 +""") + + write_if_missing(target / "Docs" / "features" / ".ai-rules.yml", + """version: 1 +file_associations: + "feature.discussion.md": "feature_discussion" + "feature.discussion.sum.md": "discussion_summary" + +rules: + feature_discussion: + outputs: + summary_companion: + path: "{dir}/discussions/feature.discussion.sum.md" + output_type: "discussion_summary_writer" + instruction: | + Ensure the summary file exists and maintain only the bounded sections: + DECISIONS, OPEN_QUESTIONS, AWAITING, ACTION_ITEMS, VOTES, TIMELINE, LINKS. + + discussion_summary: + outputs: + normalize: + path: "{dir}/feature.discussion.sum.md" + output_type: "discussion_summary_normalizer" + instruction: | + If missing, create summary with standard markers. Do not edit text outside markers. +""") + +def seed_initial_feature(target: Path, req_fields: dict | None): + today = datetime.date.today().isoformat() + fr_dir = target / "Docs" / "features" / f"FR_{today}_initial-feature-request" + disc_dir = fr_dir / "discussions" + disc_dir.mkdir(parents=True, exist_ok=True) + + if req_fields: + title = (req_fields.get("fields", {}) or {}).get("Title", "").strip() or "initialProjectDesign" + intent = (req_fields.get("fields", {}) or {}).get("Intent", "").strip() or "—" + problem = (req_fields.get("fields", {}) or {}).get("ProblemItSolves", "").strip() or "—" + brief = (req_fields.get("fields", {}) or {}).get("BriefOverview", "").strip() or "—" + summary = (req_fields.get("summary") or "").strip() + body = f"""# Feature Request: {title} + +**Intent**: {intent} +**Motivation / Problem**: {problem} +**Brief Overview**: {brief} + +**Summary**: {summary} +**Meta**: Created: {today} +""" + else: + body = (target / "process" / "templates" / "feature_request.md").read_text(encoding="utf-8") + + (fr_dir / "request.md").write_text(body, encoding="utf-8") + + (disc_dir / "feature.discussion.md").write_text( + f"""--- +type: discussion +stage: feature +status: OPEN +feature_id: FR_{today}_initial-feature-request +created: {today} +--- +## Summary +Initial discussion for the first feature request. Append your comments below. + +## Participation +- Maintainer: Kickoff. VOTE: READY +""", encoding="utf-8") + + (disc_dir / "feature.discussion.sum.md").write_text( + """# Summary — Feature + +<!-- SUMMARY:DECISIONS START --> +## Decisions (ADR-style) +- (none yet) +<!-- SUMMARY:DECISIONS END --> + +<!-- SUMMARY:OPEN_QUESTIONS START --> +## Open Questions +- (none yet) +<!-- SUMMARY:OPEN_QUESTIONS END --> + +<!-- SUMMARY:AWAITING START --> +## Awaiting Replies +- (none yet) +<!-- SUMMARY:AWAITING END --> + +<!-- SUMMARY:ACTION_ITEMS START --> +## Action Items +- (none yet) +<!-- SUMMARY:ACTION_ITEMS END --> + +<!-- SUMMARY:VOTES START --> +## Votes (latest per participant) +READY: 1 • CHANGES: 0 • REJECT: 0 +- Maintainer +<!-- SUMMARY:VOTES END --> + +<!-- SUMMARY:TIMELINE START --> +## Timeline (most recent first) +- {ts} Maintainer: Kickoff +<!-- SUMMARY:TIMELINE END --> + +<!-- SUMMARY:LINKS START --> +## Links +- Design/Plan: ../design/design.md +<!-- SUMMARY:LINKS END --> +""".replace("{ts}", today), encoding="utf-8") + +def copy_install_assets_to_target(target: Path): + # Copy DESIGN.md and ramble.py from installer if present + copy_if_exists(INSTALL_ROOT / "DESIGN.md", target / "DESIGN.md") + copy_if_exists(INSTALL_ROOT / "ramble.py", target / "ramble.py") + # Optionally copy any additional assets you drop under installer/automation, etc. + # Example: copy starter automation folder if provided in installer + if (INSTALL_ROOT / "automation").exists(): + shutil.copytree(INSTALL_ROOT / "automation", target / "automation", dirs_exist_ok=True) + +def first_commit(target: Path): + try: + sh(["git", "add", "-A"], cwd=str(target)) + sh(["git", "commit", "-m", "chore: bootstrap Cascading Development scaffolding"], cwd=str(target)) + except Exception: + pass + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--target", help="Destination path to create/use the repo") + ap.add_argument("--provider", default="mock", help="Ramble provider (default: mock)") + ap.add_argument("--no-ramble", action="store_true", help="Skip launching Ramble") + args = ap.parse_args() + + target_str = args.target + if not target_str: + target_str = input("Destination repo path (will be created if missing): ").strip() + if not target_str: + say("No target specified. Aborting.") + sys.exit(2) + + target = Path(target_str).expanduser().resolve() + target.mkdir(parents=True, exist_ok=True) + + say(f"[=] Installing Cascading Development into: {target}") + + # Copy assets from installer into target + copy_install_assets_to_target(target) + + # Ensure folder layout + for p in [ + target / "Docs" / "features", + target / "Docs" / "discussions" / "reviews", + target / "Docs" / "diagrams" / "file_diagrams", + target / "scripts" / "hooks", + target / "src", + target / "tests", + target / "process", + ]: + p.mkdir(parents=True, exist_ok=True) + + # Create rules/templates and basic process docs + seed_process_and_rules(target) + + # Initialize git & install pre-commit + ensure_git_repo(target) + install_precommit_hook(target) + + # Launch Ramble (if available) + req = None + if not args.no_ramble: + req = run_ramble_and_collect(target, provider=args.provider) + + # Seed first feature based on Ramble output + seed_initial_feature(target, req) + + # First commit + first_commit(target) + + say("[✓] Setup complete.") + say(f"Next steps:\n cd {target}\n git status") + +if __name__ == "__main__": + main()