1st commit
This commit is contained in:
parent
e914caf15f
commit
a0b2816cc5
|
|
@ -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
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
# src/cascadingdev/__init__.py
|
||||||
|
from .utils import read_version
|
||||||
|
__all__ = ["cli"]
|
||||||
|
__version__ = read_version()
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
Loading…
Reference in New Issue