1st commit

This commit is contained in:
rob 2025-10-28 21:23:08 -03:00
parent e914caf15f
commit a0b2816cc5
10 changed files with 337 additions and 194 deletions

View File

@ -0,0 +1,25 @@
version: 1
voting:
values: [READY, CHANGES, REJECT]
allow_agent_votes: true
quorum:
discussion: { ready: all, reject: all }
design: { ready: all, reject: all }
implementation: { ready: 1_human, reject: all }
testing: { ready: all, 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
timeouts:
discussion_stale_days: 3
nudge_interval_hours: 24
promotion_timeout_days: 14
security:
scanners:
enabled: true
tool: gitleaks

View File

@ -0,0 +1,62 @@
version: 1
file_associations:
"feature.discussion.md": "feature_discussion"
"feature.discussion.sum.md": "discussion_summary"
"design.discussion.md": "design_discussion"
"design.discussion.sum.md": "discussion_summary"
"implementation.discussion.md": "impl_discussion"
"implementation.discussion.sum.md":"discussion_summary"
"testing.discussion.md": "test_discussion"
"testing.discussion.sum.md": "discussion_summary"
"review.discussion.md": "review_discussion"
"review.discussion.sum.md": "discussion_summary"
rules:
feature_discussion:
outputs:
summary_companion:
path: "{dir}/discussions/feature.discussion.sum.md"
output_type: "discussion_summary_writer"
instruction: |
Keep bounded sections only: DECISIONS, OPEN_QUESTIONS, AWAITING, ACTION_ITEMS, VOTES, TIMELINE, LINKS.
design_discussion:
outputs:
summary_companion:
path: "{dir}/discussions/design.discussion.sum.md"
output_type: "discussion_summary_writer"
instruction: |
Same policy as feature; include link to ../design/design.md if present.
impl_discussion:
outputs:
summary_companion:
path: "{dir}/discussions/implementation.discussion.sum.md"
output_type: "discussion_summary_writer"
instruction: |
Same policy; include any unchecked tasks from ../implementation/tasks.md.
test_discussion:
outputs:
summary_companion:
path: "{dir}/discussions/testing.discussion.sum.md"
output_type: "discussion_summary_writer"
instruction: |
Same policy; surface FAILS either in OPEN_QUESTIONS or AWAITING.
review_discussion:
outputs:
summary_companion:
path: "{dir}/discussions/review.discussion.sum.md"
output_type: "discussion_summary_writer"
instruction: |
Same policy; record READY_FOR_RELEASE decision date if present.
discussion_summary:
outputs:
normalize:
path: "{path}"
output_type: "discussion_summary_normalizer"
instruction: |
If missing, create summary with standard markers. Never edit outside markers.

View File

@ -0,0 +1,23 @@
version: 1
# Root defaults all folders inherit unless a closer .ai-rules.yml overrides them.
file_associations:
"README.md": "readme"
"process/policies.yml": "policies"
rules:
readme:
outputs:
normalize:
path: "{repo}/README.md"
output_type: "readme_normalizer"
instruction: |
Ensure basic sections exist: Overview, Install, Usage, License. Be idempotent.
policies:
outputs:
validate:
path: "{dir}/policies.yml"
output_type: "policy_validator"
instruction: |
Validate YAML keys according to DESIGN.md Appendix A. Do not auto-edit.

View File

@ -74,7 +74,6 @@ Human → Git Commit → Pre-commit Hook → AI Generator → Markdown Artifact
│ ├─ agents.yml # Role → stages mapping │ ├─ agents.yml # Role → stages mapping
│ └─ config.yml # Configuration (future) │ └─ config.yml # Configuration (future)
├─ process/ # Process documentation & templates ├─ process/ # Process documentation & templates
│ ├─ design.md # This document
│ ├─ policies.md # Human-friendly policy documentation │ ├─ policies.md # Human-friendly policy documentation
│ ├─ policies.yml # Machine-readable policy configuration │ ├─ policies.yml # Machine-readable policy configuration
│ └─ templates/ │ └─ templates/
@ -146,7 +145,16 @@ CascadingDev/
├─ src/cascadingdev/ # core logic & optional dev CLI ├─ src/cascadingdev/ # core logic & optional dev CLI
├─ assets/ # single source of truth for shipped files ├─ assets/ # single source of truth for shipped files
│ ├─ hooks/pre-commit │ ├─ hooks/pre-commit
│ ├─ templates/{feature_request.md,discussion.md,design_doc.md} │ ├─ templates/
│ │ ├─ USER_GUIDE.md
│ │ ├─ design_doc.md
│ │ ├─ discussion.md
│ │ ├─ feature_request.md
│ │ ├─ process/
│ │ │ └─ policies.yml
│ │ └─ rules/
│ │ ├─ root.ai-rules.yml # this becomes ./.ai-rules.yml
│ │ └─ features.ai-rules.yml # this becomes Docs/features/.ai-rules.yml
│ └─ runtime/{ramble.py,create_feature.py} │ └─ runtime/{ramble.py,create_feature.py}
├─ tools/build_installer.py # creates install/cascadingdev-<version>/ ├─ tools/build_installer.py # creates install/cascadingdev-<version>/
├─ install/ # build output (git-ignored) ├─ install/ # build output (git-ignored)

View File

@ -0,0 +1,4 @@
# src/cascadingdev/__init__.py
from .utils import read_version
__all__ = ["cli"]
__version__ = read_version()

View File

@ -0,0 +1,67 @@
# src/cascadingdev/cli.py
import argparse, sys, shutil
from pathlib import Path
from . import __version__
from .utils import ROOT, read_version, bump_version, run
def main():
ap = argparse.ArgumentParser(prog="cascadingdev", description="CascadingDev CLI")
ap.add_argument("--version", action="store_true", help="Show version and exit")
sub = ap.add_subparsers(dest="cmd")
sub.add_parser("doctor", help="Check environment and templates")
sub.add_parser("smoke", help="Run smoke test")
p_build = sub.add_parser("build", help="Build installer bundle (no version bump)")
p_rel = sub.add_parser("release", help="Bump version and rebuild")
p_rel.add_argument("--kind", choices=["major","minor","patch"], default="patch")
p_pack = sub.add_parser("pack", help="Zip the current installer bundle")
p_pack.add_argument("--out", help="Output zip path (default: ./install/cascadingdev-<ver>.zip)")
args = ap.parse_args()
if args.version:
print(__version__)
return 0
if args.cmd == "doctor":
# minimal checks
required = [
ROOT / "assets" / "templates" / "USER_GUIDE.md",
ROOT / "assets" / "templates" / "rules" / "features.ai-rules.yml",
ROOT / "assets" / "hooks" / "pre-commit",
ROOT / "src" / "cascadingdev" / "setup_project.py",
]
missing = [str(p) for p in required if not p.exists()]
if missing:
print("Missing:\n " + "\n ".join(missing)); return 2
print("Doctor OK."); return 0
if args.cmd == "smoke":
return run([sys.executable, str(ROOT / "tools" / "smoke_test.py")])
if args.cmd == "build":
return run([sys.executable, str(ROOT / "tools" / "build_installer.py")])
if args.cmd == "release":
newv = bump_version(args.kind, ROOT / "VERSION")
print(f"Bumped to {newv}")
rc = run([sys.executable, str(ROOT / "tools" / "build_installer.py")])
if rc == 0:
print(f"Built installer for {newv}")
return rc
if args.cmd == "pack":
ver = read_version(ROOT / "VERSION")
bundle = ROOT / "install" / f"cascadingdev-{ver}"
if not bundle.exists():
print(f"Bundle not found: {bundle}. Run `cascadingdev build` first.")
return 2
out = Path(args.out) if args.out else (ROOT / "install" / f"cascadingdev-{ver}.zip")
if out.exists():
out.unlink()
shutil.make_archive(out.with_suffix(""), "zip", root_dir=bundle)
print(f"Packed → {out}")
return 0
ap.print_help()
return 0

View File

@ -13,12 +13,12 @@ Examples:
python setup_cascadingdev.py --target ~/dev/my-new-repo python setup_cascadingdev.py --target ~/dev/my-new-repo
python setup_cascadingdev.py --target /abs/path --no-ramble python setup_cascadingdev.py --target /abs/path --no-ramble
""" """
import sys
import json import json
import shutil
import argparse import argparse
import subprocess
import datetime import datetime
import sys
import subprocess
import shutil
from pathlib import Path from pathlib import Path
# Bundle root (must contain assets/, ramble.py, VERSION) # Bundle root (must contain assets/, ramble.py, VERSION)
@ -32,42 +32,42 @@ if not (INSTALL_ROOT / "assets").exists():
# ---------- Helper Functions ---------- # ---------- Helper Functions ----------
def sh(cmd, check=True, cwd=None): def say(msg: str) -> None:
"""Run a shell command and return the completed process."""
return subprocess.run(cmd, check=check, text=True, capture_output=True, cwd=cwd)
def say(msg):
"""Print a message with immediate flush."""
print(msg, flush=True) print(msg, flush=True)
def ensure_dir(p: Path) -> None:
p.mkdir(parents=True, exist_ok=True)
def write_if_missing(path: Path, content: str): def write_if_missing(path: Path, content: str) -> None:
"""Write content to a file only if it doesn't already exist.""" ensure_dir(path.parent)
path.parent.mkdir(parents=True, exist_ok=True)
if not path.exists(): if not path.exists():
path.write_text(content, encoding="utf-8") path.write_text(content, encoding="utf-8")
def copy_if_exists(src: Path, dst: Path) -> None:
def copy_if_exists(src: Path, dst: Path):
"""Copy a file from source to destination if the source exists."""
if src.exists(): if src.exists():
dst.parent.mkdir(parents=True, exist_ok=True) ensure_dir(dst.parent)
shutil.copy2(str(src), str(dst)) shutil.copy2(src, dst)
def copy_if_missing(src: Path, dst: Path) -> None:
ensure_dir(dst.parent)
if not dst.exists():
shutil.copy2(src, dst)
def run(cmd: list[str], cwd: Path | None = None) -> int:
proc = subprocess.Popen(cmd, cwd=cwd, stdout=sys.stdout, stderr=sys.stderr)
return proc.wait()
def ensure_git_repo(target: Path): def ensure_git_repo(target: Path):
"""Initialize a git repository if one doesn't exist at the target path.""" """Initialize a git repository if one doesn't exist at the target path."""
if not (target / ".git").exists(): if not (target / ".git").exists():
# Initialize git repo with main branch # Initialize git repo with main branch
sh(["git", "init", "-b", "main"], cwd=str(target)) run(["git", "init", "-b", "main"], cwd=target)
# Create basic .gitignore file # Create basic .gitignore file
write_if_missing(target / ".gitignore", "\n".join([ write_if_missing(target / ".gitignore", "\n".join([
".env", ".env.*", "secrets/", ".git/ai-rules-*", "__pycache__/", ".env", ".env.*", "secrets/", ".git/ai-rules-*", "__pycache__/",
"*.pyc", ".pytest_cache/", ".DS_Store", "*.pyc", ".pytest_cache/", ".DS_Store",
]) + "\n") ]) + "\n")
def install_precommit_hook(target: Path): def install_precommit_hook(target: Path):
"""Install the pre-commit hook from installer assets to target git hooks.""" """Install the pre-commit hook from installer assets to target git hooks."""
hook_src = INSTALL_ROOT / "assets" / "hooks" / "pre-commit" hook_src = INSTALL_ROOT / "assets" / "hooks" / "pre-commit"
@ -143,50 +143,31 @@ def run_ramble_and_collect(target: Path, provider: str = "mock", claude_cmd: str
def seed_process_and_rules(target: Path): def seed_process_and_rules(target: Path):
"""Create minimal, machine-readable rules that let the FIRST FEATURE define the project.""" """Seed machine-readable policies and stage rules by copying installer templates."""
# Seed process/policies.yml (machine-readable), per DESIGN.md Appendix A
process_dir = target / "process"
rules_dir = target / "Docs" / "features"
process_dir.mkdir(parents=True, exist_ok=True)
rules_dir.mkdir(parents=True, exist_ok=True)
# Create AI rules configuration for general markdown files # Locate templates in THIS installer bundle
write_if_missing(target / ".ai-rules.yml", t_root = INSTALL_ROOT / "assets" / "templates"
"""version: 1 t_process = t_root / "process" / "policies.yml"
file_associations: t_rules_root = t_root / "rules" / "root.ai-rules.yml"
"*.md": "md-file" t_rules_features = t_root / "rules" / "features.ai-rules.yml"
rules: # Copy policies
md-file: if t_process.exists():
description: "Normalize Markdown" copy_if_missing(t_process, process_dir / "policies.yml")
instruction: |
Keep markdown tidy (headings, lists, spacing). No content churn.
settings:
model: "local-mock"
temperature: 0.1
""")
# Create AI rules specific to feature discussions # Copy rules files into expected locations
write_if_missing(target / "Docs" / "features" / ".ai-rules.yml", # Root rules (optional if you want a project-wide baseline)
"""version: 1 if t_rules_root.exists():
file_associations: copy_if_missing(t_rules_root, target / ".ai-rules.yml")
"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.
""")
# Discussion/feature rules (cascade/override within Docs/features)
if t_rules_features.exists():
copy_if_missing(t_rules_features, rules_dir / ".ai-rules.yml")
def seed_initial_feature(target: Path, req_fields: dict | None): def seed_initial_feature(target: Path, req_fields: dict | None):
"""Create the initial feature request and associated discussion files.""" """Create the initial feature request and associated discussion files."""
@ -307,8 +288,8 @@ def copy_install_assets_to_target(target: Path):
def first_commit(target: Path): def first_commit(target: Path):
"""Perform the initial git commit of all scaffolded files.""" """Perform the initial git commit of all scaffolded files."""
try: try:
sh(["git", "add", "-A"], cwd=str(target)) run(["git", "add", "-A"], cwd=target)
sh(["git", "commit", "-m", "chore: bootstrap Cascading Development scaffolding"], cwd=str(target)) run(["git", "commit", "-m", "chore: bootstrap Cascading Development scaffolding"], cwd=target)
except Exception: except Exception:
# Silently continue if commit fails (e.g., no git config) # Silently continue if commit fails (e.g., no git config)
pass pass

View File

@ -0,0 +1,62 @@
from __future__ import annotations
from pathlib import Path
import shutil, subprocess, sys, re
ROOT = Path(__file__).resolve().parents[2] # repo root
def say(msg: str) -> None:
print(msg, flush=True)
def ensure_dir(p: Path) -> None:
p.mkdir(parents=True, exist_ok=True)
def write_if_missing(path: Path, content: str) -> None:
ensure_dir(path.parent)
if not path.exists():
path.write_text(content, encoding="utf-8")
def copy_if_exists(src: Path, dst: Path) -> None:
if src.exists():
ensure_dir(dst.parent)
shutil.copy2(src, dst)
def copy_if_missing(src: Path, dst: Path) -> None:
dst.parent.mkdir(parents=True, exist_ok=True)
if not dst.exists():
shutil.copy2(src, dst)
def run(cmd: list[str], cwd: Path | None = None) -> int:
proc = subprocess.Popen(cmd, cwd=cwd, stdout=sys.stdout, stderr=sys.stderr)
return proc.wait()
def read_version(version_file: Path | None = None) -> str:
vf = version_file or (ROOT / "VERSION")
return (vf.read_text(encoding="utf-8").strip() if vf.exists() else "0.0.0")
def write_version(new_version: str, version_file: Path | None = None) -> None:
vf = version_file or (ROOT / "VERSION")
vf.write_text(new_version.strip() + "\n", encoding="utf-8")
_semver = re.compile(r"^(\d+)\.(\d+)\.(\d+)$")
def bump_version(kind: str = "patch", version_file: Path | None = None) -> str:
cur = read_version(version_file)
m = _semver.match(cur) or _semver.match("0.1.0") # default if missing
major, minor, patch = map(int, m.groups())
if kind == "major":
major, minor, patch = major + 1, 0, 0
elif kind == "minor":
minor, patch = minor + 1, 0
else:
patch += 1
new = f"{major}.{minor}.{patch}"
write_version(new, version_file)
return new
def bundle_path(version_file: Path | None = None) -> Path:
"""
Return the install bundle path for the current VERSION (e.g., install/cascadingdev-0.1.2).
Raises FileNotFoundError if missing.
"""
ver = read_version(version_file)
bp = ROOT / "install" / f"cascadingdev-{ver}"
if not bp.exists():
raise FileNotFoundError(f"Bundle not found: {bp}. Build it with `cascadingdev build`.")
return bp

View File

@ -8,149 +8,38 @@ VER = (ROOT / "VERSION").read_text().strip() if (ROOT / "VERSION").exists() els
BUNDLE = OUT / f"cascadingdev-{VER}" BUNDLE = OUT / f"cascadingdev-{VER}"
def main(): def main():
# Removes the old install bundle if it already exists
if BUNDLE.exists(): if BUNDLE.exists():
shutil.rmtree(BUNDLE) shutil.rmtree(BUNDLE)
# copy essentials # Create the directories
(BUNDLE / "assets" / "hooks").mkdir(parents=True, exist_ok=True) (BUNDLE / "assets" / "hooks").mkdir(parents=True, exist_ok=True)
(BUNDLE / "assets" / "templates").mkdir(parents=True, exist_ok=True) (BUNDLE / "assets" / "templates").mkdir(parents=True, exist_ok=True)
shutil.copy2(ROOT / "DESIGN.md", BUNDLE / "DESIGN.md") # Copy the git hook and any other runtime utilities.
shutil.copy2(ROOT / "assets" / "runtime" / "ramble.py", BUNDLE / "ramble.py") shutil.copy2(ROOT / "assets" / "runtime" / "ramble.py", BUNDLE / "ramble.py")
shutil.copy2(ROOT / "assets" / "runtime" / "create_feature.py", BUNDLE / "create_feature.py")
shutil.copy2(ROOT / "assets" / "hooks" / "pre-commit", BUNDLE / "assets" / "hooks" / "pre-commit") shutil.copy2(ROOT / "assets" / "hooks" / "pre-commit", BUNDLE / "assets" / "hooks" / "pre-commit")
for t in ["feature_request.md","discussion.md","design_doc.md"]:
# copy core templates
for t in ["feature_request.md","discussion.md","design_doc.md","USER_GUIDE.md"]:
shutil.copy2(ROOT / "assets" / "templates" / t, BUNDLE / "assets" / "templates" / t) shutil.copy2(ROOT / "assets" / "templates" / t, BUNDLE / "assets" / "templates" / t)
# copy (recursively) the contents of process/ and rules/ templates folders
def copy_tree(src, dst):
if dst.exists():
shutil.rmtree(dst)
shutil.copytree(src, dst)
copy_tree(ROOT / "assets" / "templates" / "process", BUNDLE / "assets" / "templates" / "process")
copy_tree(ROOT / "assets" / "templates" / "rules", BUNDLE / "assets" / "templates" / "rules")
# write installer entrypoint # write installer entrypoint
(BUNDLE / "setup_cascadingdev.py").write_text(INSTALLER_PY, encoding="utf-8") shutil.copy2(ROOT / "src" / "cascadingdev" / "setup_project.py",
BUNDLE / "setup_cascadingdev.py")
(BUNDLE / "INSTALL.md").write_text("Unzip, then run:\n\n python3 setup_cascadingdev.py\n", encoding="utf-8") (BUNDLE / "INSTALL.md").write_text("Unzip, then run:\n\n python3 setup_cascadingdev.py\n", encoding="utf-8")
(BUNDLE / "VERSION").write_text(VER, encoding="utf-8") (BUNDLE / "VERSION").write_text(VER, encoding="utf-8")
print(f"[✓] Built installer → {BUNDLE}") print(f"[✓] Built installer → {BUNDLE}")
INSTALLER_PY = r'''#!/usr/bin/env python3
import argparse, json, os, shutil, subprocess, sys, datetime
from pathlib import Path
HERE = Path(__file__).resolve().parent
def sh(cmd, cwd=None):
return subprocess.run(cmd, check=True, text=True, capture_output=True, cwd=cwd)
def say(x): print(x, flush=True)
def write_if_missing(p: Path, content: str):
p.parent.mkdir(parents=True, exist_ok=True)
if not p.exists():
p.write_text(content, encoding="utf-8")
def copytree(src: Path, dst: Path):
dst.parent.mkdir(parents=True, exist_ok=True)
if src.is_file():
shutil.copy2(src, dst)
else:
shutil.copytree(src, dst, dirs_exist_ok=True)
def ensure_git_repo(target: Path):
if not (target / ".git").exists():
sh(["git", "init", "-b", "main"], cwd=target)
write_if_missing(target / ".gitignore", ".env\n.env.*\nsecrets/\n__pycache__/\n*.pyc\n.pytest_cache/\n.DS_Store\n")
def install_hook(target: Path):
hooks = target / ".git" / "hooks"; hooks.mkdir(parents=True, exist_ok=True)
src = HERE / "assets" / "hooks" / "pre-commit"
dst = hooks / "pre-commit"
dst.write_text(src.read_text(encoding="utf-8"), encoding="utf-8")
dst.chmod(0o755)
def run_ramble(target: Path, provider="mock"):
ramble = target / "ramble.py"
if not ramble.exists(): 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…")
p = subprocess.run(args, text=True, capture_output=True, cwd=target)
try:
return json.loads((p.stdout or "").strip())
except Exception:
say("[-] Could not parse Ramble output; using template defaults.")
return None
def seed_rules_and_templates(target: Path):
write_if_missing(target / ".ai-rules.yml",
"version: 1\nfile_associations:\n \"*.md\": \"md-file\"\n\nrules:\n md-file:\n description: \"Normalize Markdown\"\n instruction: |\n Keep markdown tidy (headings, lists, spacing). No content churn.\nsettings:\n model: \"local-mock\"\n temperature: 0.1\n")
# copy templates
copytree(HERE / "assets" / "templates", target / "process" / "templates")
def seed_first_feature(target: Path, req):
today = datetime.date.today().isoformat()
fr = target / "Docs" / "features" / f"FR_{today}_initial-feature-request"
disc = fr / "discussions"; disc.mkdir(parents=True, exist_ok=True)
if req:
fields = req.get("fields", {}) or {}
title = (fields.get("Title") or "initialProjectDesign").strip()
intent = (fields.get("Intent") or "").strip()
problem = (fields.get("ProblemItSolves") or "").strip()
brief = (fields.get("BriefOverview") or "").strip()
summary = (req.get("summary") or "").strip()
body = f"# Feature Request: {title}\n\n**Intent**: {intent}\n**Motivation / Problem**: {problem}\n**Brief Overview**: {brief}\n\n**Summary**: {summary}\n**Meta**: Created: {today}\n"
else:
body = (target / "process" / "templates" / "feature_request.md").read_text(encoding="utf-8")
(fr / "request.md").write_text(body, encoding="utf-8")
(disc / "feature.discussion.md").write_text(
f"---\ntype: discussion\nstage: feature\nstatus: OPEN\nfeature_id: FR_{today}_initial-feature-request\ncreated: {today}\n---\n## Summary\nKickoff discussion. Append comments below.\n\n## Participation\n- Maintainer: Kickoff. VOTE: READY\n", encoding="utf-8")
(disc / "feature.discussion.sum.md").write_text(
"# Summary — Feature\n\n<!-- SUMMARY:DECISIONS START -->\n## Decisions (ADR-style)\n- (none yet)\n<!-- SUMMARY:DECISIONS END -->\n\n<!-- SUMMARY:OPEN_QUESTIONS START -->\n## Open Questions\n- (none yet)\n<!-- SUMMARY:OPEN_QUESTIONS END -->\n\n<!-- SUMMARY:AWAITING START -->\n## Awaiting Replies\n- (none yet)\n<!-- SUMMARY:AWAITING END -->\n\n<!-- SUMMARY:ACTION_ITEMS START -->\n## Action Items\n- (none yet)\n<!-- SUMMARY:ACTION_ITEMS END -->\n\n<!-- SUMMARY:VOTES START -->\n## Votes (latest per participant)\nREADY: 1 • CHANGES: 0 • REJECT: 0\n- Maintainer\n<!-- SUMMARY:VOTES END -->\n\n<!-- SUMMARY:TIMELINE START -->\n## Timeline (most recent first)\n- {today} Maintainer: Kickoff\n<!-- SUMMARY:TIMELINE END -->\n\n<!-- SUMMARY:LINKS START -->\n## Links\n- Design/Plan: ../design/design.md\n<!-- SUMMARY:LINKS END -->\n".replace("{today}", today), encoding="utf-8")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--target", help="Destination path for the user's project")
ap.add_argument("--no-ramble", action="store_true")
ap.add_argument("--provider", default="mock")
args = ap.parse_args()
target = Path(args.target or input("User's project folder: ").strip()).expanduser().resolve()
target.mkdir(parents=True, exist_ok=True)
say(f"[=] Installing into: {target}")
# copy top-level assets
shutil.copy2(HERE / "DESIGN.md", target / "DESIGN.md")
shutil.copy2(HERE / "ramble.py", target / "ramble.py")
# basic tree
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)
# rules / templates
seed_rules_and_templates(target)
# git + hook
ensure_git_repo(target)
install_hook(target)
# ramble
req = None if args.no_ramble else run_ramble(target, provider=args.provider)
# seed FR
seed_first_feature(target, req)
try:
sh(["git", "add", "-A"], cwd=target)
sh(["git", "commit", "-m", "chore: bootstrap Cascading Development scaffolding"], cwd=target)
except Exception:
pass
say("[✓] Done. Next:\n cd " + str(target) + "\n git status")
if __name__ == "__main__":
main()
'''
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@ -0,0 +1,22 @@
#!/usr/bin/env python3
from pathlib import Path
def main():
root = Path(__file__).resolve().parents[1]
required = [
root / "assets" / "hooks" / "pre-commit",
root / "assets" / "templates" / "feature_request.md",
root / "assets" / "templates" / "discussion.md",
root / "assets" / "templates" / "design_doc.md",
root / "assets" / "templates" / "USER_GUIDE.md", # now required
root / "assets" / "runtime" / "ramble.py",
root / "tools" / "build_installer.py",
root / "src" / "cascadingdev" / "setup_project.py",
]
missing = [str(p) for p in required if not p.exists()]
if missing:
print("Missing:", *missing, sep="\n ")
raise SystemExit(2)
print("Smoke OK.")
if __name__ == "__main__":
main()