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
|
||||
│ └─ config.yml # Configuration (future)
|
||||
├─ process/ # Process documentation & templates
|
||||
│ ├─ design.md # This document
|
||||
│ ├─ policies.md # Human-friendly policy documentation
|
||||
│ ├─ policies.yml # Machine-readable policy configuration
|
||||
│ └─ templates/
|
||||
|
|
@ -146,7 +145,16 @@ CascadingDev/
|
|||
├─ src/cascadingdev/ # core logic & optional dev CLI
|
||||
├─ assets/ # single source of truth for shipped files
|
||||
│ ├─ 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}
|
||||
├─ tools/build_installer.py # creates install/cascadingdev-<version>/
|
||||
├─ 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 /abs/path --no-ramble
|
||||
"""
|
||||
import sys
|
||||
import json
|
||||
import shutil
|
||||
import argparse
|
||||
import subprocess
|
||||
import datetime
|
||||
import sys
|
||||
import subprocess
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
# Bundle root (must contain assets/, ramble.py, VERSION)
|
||||
|
|
@ -32,42 +32,42 @@ if not (INSTALL_ROOT / "assets").exists():
|
|||
|
||||
# ---------- Helper Functions ----------
|
||||
|
||||
def sh(cmd, check=True, cwd=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."""
|
||||
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):
|
||||
"""Write content to a file only if it doesn't already exist."""
|
||||
path.parent.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):
|
||||
"""Copy a file from source to destination if the source exists."""
|
||||
def copy_if_exists(src: Path, dst: Path) -> None:
|
||||
if src.exists():
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(str(src), str(dst))
|
||||
ensure_dir(dst.parent)
|
||||
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):
|
||||
"""Initialize a git repository if one doesn't exist at the target path."""
|
||||
if not (target / ".git").exists():
|
||||
# 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
|
||||
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):
|
||||
"""Install the pre-commit hook from installer assets to target git hooks."""
|
||||
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):
|
||||
"""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
|
||||
write_if_missing(target / ".ai-rules.yml",
|
||||
"""version: 1
|
||||
file_associations:
|
||||
"*.md": "md-file"
|
||||
# Locate templates in THIS installer bundle
|
||||
t_root = INSTALL_ROOT / "assets" / "templates"
|
||||
t_process = t_root / "process" / "policies.yml"
|
||||
t_rules_root = t_root / "rules" / "root.ai-rules.yml"
|
||||
t_rules_features = t_root / "rules" / "features.ai-rules.yml"
|
||||
|
||||
rules:
|
||||
md-file:
|
||||
description: "Normalize Markdown"
|
||||
instruction: |
|
||||
Keep markdown tidy (headings, lists, spacing). No content churn.
|
||||
settings:
|
||||
model: "local-mock"
|
||||
temperature: 0.1
|
||||
""")
|
||||
# Copy policies
|
||||
if t_process.exists():
|
||||
copy_if_missing(t_process, process_dir / "policies.yml")
|
||||
|
||||
# Create AI rules specific to feature discussions
|
||||
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.
|
||||
""")
|
||||
# Copy rules files into expected locations
|
||||
# Root rules (optional if you want a project-wide baseline)
|
||||
if t_rules_root.exists():
|
||||
copy_if_missing(t_rules_root, target / ".ai-rules.yml")
|
||||
|
||||
# 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):
|
||||
"""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):
|
||||
"""Perform the initial git commit of all scaffolded files."""
|
||||
try:
|
||||
sh(["git", "add", "-A"], cwd=str(target))
|
||||
sh(["git", "commit", "-m", "chore: bootstrap Cascading Development scaffolding"], cwd=str(target))
|
||||
run(["git", "add", "-A"], cwd=target)
|
||||
run(["git", "commit", "-m", "chore: bootstrap Cascading Development scaffolding"], cwd=target)
|
||||
except Exception:
|
||||
# Silently continue if commit fails (e.g., no git config)
|
||||
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}"
|
||||
|
||||
def main():
|
||||
# Removes the old install bundle if it already exists
|
||||
if BUNDLE.exists():
|
||||
shutil.rmtree(BUNDLE)
|
||||
# copy essentials
|
||||
# Create the directories
|
||||
(BUNDLE / "assets" / "hooks").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" / "create_feature.py", BUNDLE / "create_feature.py")
|
||||
|
||||
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)
|
||||
|
||||
# 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
|
||||
(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 / "VERSION").write_text(VER, encoding="utf-8")
|
||||
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__":
|
||||
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