Compare commits
10 Commits
5506891a52
...
e37b1a9722
| Author | SHA1 | Date |
|---|---|---|
|
|
e37b1a9722 | |
|
|
536d885b6b | |
|
|
d78b0d83c8 | |
|
|
db728ac2e4 | |
|
|
ea38549348 | |
|
|
a0b2816cc5 | |
|
|
e914caf15f | |
|
|
67a4415600 | |
|
|
ade5f91ad7 | |
|
|
a075472a98 |
|
|
@ -2,3 +2,4 @@
|
|||
.idea/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
install/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Overview
|
||||
|
||||
**CascadingDev (CDev) - Simplified** is the core of a Git-native AI-human collaboration framework. This simplified version focuses on:
|
||||
|
||||
- Git pre-commit hooks with safety checks
|
||||
- Cascading `.ai-rules.yml` system
|
||||
- Ramble GUI for structured feature requests
|
||||
- Installer bundle generation
|
||||
|
||||
For advanced discussion orchestration, see [Orchestrated Discussions](https://gitea.brrd.tech/rob/orchestrated-discussions).
|
||||
|
||||
### Key Concept: Two Repositories
|
||||
|
||||
- **CascadingDev repo** (this codebase): The tooling that builds installer bundles
|
||||
- **User's project repo**: A new repository scaffolded by running the installer bundle
|
||||
|
||||
## Repository Architecture
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
CascadingDev/
|
||||
├── src/cascadingdev/ # Core Python modules and CLI
|
||||
│ ├── cli.py # Main CLI entry point (cdev command)
|
||||
│ ├── setup_project.py # Installer script (copied to bundle)
|
||||
│ └── utils.py # Shared utilities
|
||||
├── assets/ # Single source of truth for shipped files
|
||||
│ ├── hooks/pre-commit # Git hook template (bash script)
|
||||
│ ├── templates/ # Markdown templates copied to user projects
|
||||
│ │ ├── rules/ # .ai-rules.yml files
|
||||
│ │ └── process/ # policies.yml
|
||||
│ └── runtime/ # Python scripts copied to user projects
|
||||
│ ├── ramble.py # GUI for feature creation (PySide6/PyQt5)
|
||||
│ └── create_feature.py # CLI for feature creation
|
||||
├── tools/ # Build and test scripts
|
||||
│ ├── build_installer.py # Creates install/ bundle
|
||||
│ └── smoke_test.py # Basic validation
|
||||
├── install/ # Build output (git-ignored)
|
||||
└── VERSION # Semantic version
|
||||
```
|
||||
|
||||
## Common Commands
|
||||
|
||||
```bash
|
||||
# Initial setup
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install --upgrade pip wheel PySide6
|
||||
|
||||
# Install in development mode
|
||||
pip install -e .
|
||||
|
||||
# Build the installer bundle
|
||||
cdev build
|
||||
|
||||
# Test-install into a temporary folder
|
||||
python install/cascadingdev-*/setup_cascadingdev.py --target /tmp/myproject --no-ramble
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Cascading Rules System
|
||||
|
||||
The `.ai-rules.yml` files define automation behavior. User projects have:
|
||||
- Root `.ai-rules.yml` - Global defaults
|
||||
- `Docs/features/.ai-rules.yml` - Feature-specific rules
|
||||
|
||||
Rules are hierarchical: nearest file takes precedence.
|
||||
|
||||
### Pre-commit Hook
|
||||
|
||||
The bash pre-commit hook (`assets/hooks/pre-commit`) provides:
|
||||
- Scans for potential secrets (blocks commit on match)
|
||||
- Ensures discussion files have companion `.sum.md` summary files
|
||||
- Uses flock to prevent git corruption from concurrent commits
|
||||
- Fast and lightweight (pure bash, no Python dependencies)
|
||||
|
||||
Environment variables:
|
||||
- `CDEV_SKIP_HOOK=1` - Skip all hook checks
|
||||
- `CDEV_SKIP_SUMMARIES=1` - Skip summary file generation
|
||||
|
||||
### Build System
|
||||
|
||||
The build process (`tools/build_installer.py`) creates a standalone installer bundle:
|
||||
1. Reads version from `VERSION` file
|
||||
2. Creates `install/cascadingdev-<version>/` directory
|
||||
3. Copies essential files from `assets/` to bundle
|
||||
4. Copies `src/cascadingdev/setup_project.py` as the installer entry point
|
||||
|
||||
## Related Projects
|
||||
|
||||
This project is part of a stack:
|
||||
|
||||
1. **[SmartTools](https://gitea.brrd.tech/rob/SmartTools)** - AI provider abstraction
|
||||
2. **[Orchestrated Discussions](https://gitea.brrd.tech/rob/orchestrated-discussions)** - Multi-agent discussion orchestration
|
||||
3. **[Ramble](https://gitea.brrd.tech/rob/ramble)** - AI-powered structured field extraction GUI
|
||||
4. **[Artifact Editor](https://gitea.brrd.tech/rob/artifact-editor)** - AI-enhanced diagram and model creation
|
||||
76
README.md
76
README.md
|
|
@ -0,0 +1,76 @@
|
|||
# CascadingDev (CDev) - Simplified
|
||||
|
||||
**CDev** — short for *Cascading Development* — is a **Git-native AI–human collaboration framework** that uses git hooks and cascading rules to enhance your development workflow.
|
||||
|
||||
This is the **simplified version** focused on core functionality:
|
||||
- Git pre-commit hooks with safety checks
|
||||
- Cascading `.ai-rules.yml` system
|
||||
- Ramble GUI for capturing structured feature requests
|
||||
|
||||
For advanced discussion orchestration features, see [Orchestrated Discussions](https://gitea.brrd.tech/rob/orchestrated-discussions).
|
||||
|
||||
---
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Cascading Rules System** — nearest `.ai-rules.yml` defines behavior at each directory level
|
||||
- **Pre-commit Hook** — secret scanning, discussion summary creation, git corruption prevention
|
||||
- **Ramble GUI** — PySide6/PyQt5 dialog for capturing structured feature requests
|
||||
- **Deterministic Builds** — reproducible installer bundle
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Create and activate a virtual environment
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install --upgrade pip wheel PySide6
|
||||
|
||||
# 2. Build the installer bundle
|
||||
python tools/build_installer.py
|
||||
|
||||
# 3. Install into a project folder
|
||||
python install/cascadingdev-*/setup_cascadingdev.py --target /path/to/myproject
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
CascadingDev/
|
||||
├── assets/
|
||||
│ ├── hooks/pre-commit # Git pre-commit hook
|
||||
│ ├── runtime/ # Runtime scripts (ramble.py, create_feature.py)
|
||||
│ └── templates/ # Discussion and rule templates
|
||||
├── src/cascadingdev/ # Python package
|
||||
│ ├── setup_project.py # Project initialization
|
||||
│ ├── cli.py # Command-line interface
|
||||
│ └── ...
|
||||
├── tools/ # Build and test tools
|
||||
└── docs/ # Documentation
|
||||
```
|
||||
|
||||
## Pre-commit Hook Features
|
||||
|
||||
The pre-commit hook provides:
|
||||
|
||||
1. **Secret Scanning** - Prevents accidental commit of API keys and secrets
|
||||
2. **Summary Files** - Auto-creates `.sum.md` companion files for discussions
|
||||
3. **Concurrency Safety** - Uses flock to prevent git corruption from parallel commits
|
||||
|
||||
Environment variables:
|
||||
- `CDEV_SKIP_HOOK=1` - Skip all hook checks
|
||||
- `CDEV_SKIP_SUMMARIES=1` - Skip summary file generation
|
||||
|
||||
## Related Projects
|
||||
|
||||
This project is part of a three-layer stack:
|
||||
|
||||
1. **[SmartTools](https://gitea.brrd.tech/rob/SmartTools)** - AI provider abstraction and tool execution
|
||||
2. **[Orchestrated Discussions](https://gitea.brrd.tech/rob/orchestrated-discussions)** - Multi-agent discussion orchestration
|
||||
3. **[Ramble](https://gitea.brrd.tech/rob/ramble)** - AI-powered structured field extraction GUI
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
|
@ -1,18 +1,59 @@
|
|||
#!/usr/bin/env bash
|
||||
# Safety settings: exit on errors, treat unset variables as errors, and catch pipeline failures
|
||||
#
|
||||
# CascadingDev Pre-commit Hook
|
||||
# =============================
|
||||
# This hook provides safety checks during git commits.
|
||||
#
|
||||
# What it does:
|
||||
# 1. Scans for potential secrets in staged changes
|
||||
# 2. Creates companion summary files (.sum.md) for discussion files
|
||||
#
|
||||
# Environment Variables:
|
||||
# CDEV_SKIP_HOOK=1 Skip all checks (hook exits immediately)
|
||||
# CDEV_SKIP_SUMMARIES=1 Skip summary file generation
|
||||
#
|
||||
# Safety: Exits on errors to prevent broken commits
|
||||
set -euo pipefail
|
||||
|
||||
# Find and navigate to the git repo root (or current dir if not in a repo) so file paths work correctly regardless of where the commit command is run
|
||||
if [[ -n "${CDEV_SKIP_HOOK:-}" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Navigate to git repository root so all file paths work correctly
|
||||
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo ".")"
|
||||
cd "$ROOT"
|
||||
|
||||
# ============================================================================
|
||||
# CRITICAL: Acquire Hook Execution Lock
|
||||
# ============================================================================
|
||||
# Prevents concurrent hook executions from corrupting Git repository.
|
||||
# Race condition scenario:
|
||||
# - Process A runs `git add file1.md`, computes blob SHA, starts writing to .git/objects/
|
||||
# - Process B runs `git add file2.md` concurrently
|
||||
# - Blob object creation fails, leaving orphaned SHA in index
|
||||
# - Result: "error: invalid object 100644 <SHA> for '<file>'"
|
||||
#
|
||||
# Solution: Use flock to ensure only one hook instance runs at a time.
|
||||
# The lock is automatically released when this script exits.
|
||||
# ============================================================================
|
||||
LOCK_FILE="${ROOT}/.git/hooks/pre-commit.lock"
|
||||
exec 9>"$LOCK_FILE"
|
||||
|
||||
if ! flock -n 9; then
|
||||
echo >&2 "[pre-commit] Another pre-commit hook is running. Waiting for lock..."
|
||||
flock 9 # Block until lock is available
|
||||
echo >&2 "[pre-commit] Lock acquired, continuing..."
|
||||
fi
|
||||
|
||||
# Cleanup: Remove lock file on exit
|
||||
trap 'rm -f "$LOCK_FILE"' EXIT
|
||||
|
||||
# -------- collect staged files ----------
|
||||
# Get list of staged added/modified files into STAGED array, exit early if none found
|
||||
mapfile -t STAGED < <(git diff --cached --name-only --diff-filter=AM || true)
|
||||
[ "${#STAGED[@]}" -eq 0 ] && exit 0
|
||||
|
||||
# -------- tiny secret scan (fast, regex only) ----------
|
||||
# Abort commit if staged changes contain potential secrets (api keys, tokens, etc.) matching common patterns
|
||||
# Abort commit if staged changes contain potential secrets matching common patterns
|
||||
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."
|
||||
|
|
@ -21,7 +62,7 @@ if echo "$DIFF" | grep -Eqi '(api[_-]?key|secret|access[_-]?token|private[_-]?ke
|
|||
fi
|
||||
|
||||
# -------- ensure discussion summaries exist (companion files) ----------
|
||||
# Create and auto-stage a summary template file for any discussion file that doesn't already have one
|
||||
if [[ -z "${CDEV_SKIP_SUMMARIES:-}" ]]; then
|
||||
ensure_summary() {
|
||||
local disc="$1"
|
||||
local dir; dir="$(dirname "$disc")"
|
||||
|
|
@ -78,11 +119,6 @@ for f in "${STAGED[@]}"; do
|
|||
Docs/features/*/discussions/*.discussion.md) ensure_summary "$f";;
|
||||
esac
|
||||
done
|
||||
|
||||
# -------- future orchestration (non-blocking status) ----------
|
||||
# Run workflow status check if available, but don't block commit if it fails
|
||||
if [ -x "automation/workflow.py" ]; then
|
||||
python3 automation/workflow.py --status || true
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
|
|
|||
|
|
@ -0,0 +1,262 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
create_feature.py — create a new feature request (+ discussion & summary)
|
||||
|
||||
Usage:
|
||||
python create_feature.py --title "My Idea"
|
||||
python create_feature.py --no-ramble
|
||||
python create_feature.py --dir /path/to/repo
|
||||
|
||||
Behavior:
|
||||
- Prefer Ramble (ramble.py in repo root) unless --no-ramble is passed.
|
||||
- If Ramble not present or fails, prompt for fields in terminal.
|
||||
- Fields come from the feature_request.md template when possible.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import argparse, datetime, json, os, re, subprocess, sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
# --------- helpers ---------
|
||||
def say(msg: str) -> None:
|
||||
print(msg, flush=True)
|
||||
|
||||
def git_root_or_cwd(start: Path) -> Path:
|
||||
try:
|
||||
cp = subprocess.run(["git", "rev-parse", "--show-toplevel"],
|
||||
text=True, capture_output=True, check=True, cwd=start)
|
||||
return Path(cp.stdout.strip())
|
||||
except Exception:
|
||||
return start
|
||||
|
||||
def read_text(p: Path) -> str:
|
||||
return p.read_text(encoding="utf-8") if p.exists() else ""
|
||||
|
||||
def write_text(p: Path, s: str) -> None:
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(s, encoding="utf-8")
|
||||
|
||||
def slugify(s: str) -> str:
|
||||
s = s.strip().lower()
|
||||
s = re.sub(r"[^a-z0-9]+", "-", s)
|
||||
s = re.sub(r"-{2,}", "-", s).strip("-")
|
||||
return s or "feature"
|
||||
|
||||
def today() -> str:
|
||||
return datetime.date.today().isoformat()
|
||||
|
||||
def find_template_fields(tmpl: str) -> List[Tuple[str, str]]:
|
||||
"""
|
||||
Scan template for lines like:
|
||||
**Intent**: <...>
|
||||
Return list of (FieldName, placeholderText).
|
||||
"""
|
||||
fields = []
|
||||
for m in re.finditer(r"^\s*\*\*(.+?)\*\*:\s*(<[^>]+>|.*)$", tmpl, flags=re.M):
|
||||
label = m.group(1).strip()
|
||||
placeholder = m.group(2).strip()
|
||||
# skip meta/system fields the script will generate
|
||||
if label.lower().startswith("feature id") or label.lower().startswith("meta"):
|
||||
continue
|
||||
fields.append((label, placeholder))
|
||||
return fields
|
||||
|
||||
def default_fields() -> List[str]:
|
||||
return ["Title", "Intent", "Motivation / Problem", "Constraints / Non-Goals",
|
||||
"Rough Proposal", "Open Questions", "Author"]
|
||||
|
||||
def collect_via_prompts(field_labels: List[str]) -> Dict[str, str]:
|
||||
say("[•] Ramble disabled or not found; collecting fields in terminal…")
|
||||
out = {}
|
||||
for label in field_labels:
|
||||
try:
|
||||
val = input(f"{label}: ").strip()
|
||||
except EOFError:
|
||||
val = ""
|
||||
out[label] = val
|
||||
if "Title" not in out or not out["Title"].strip():
|
||||
out["Title"] = "initialProjectDesign"
|
||||
return out
|
||||
|
||||
def try_ramble(repo_root: Path, field_labels: List[str], provider: str, claude_cmd: str) -> Dict[str, str] | None:
|
||||
ramble = repo_root / "ramble.py"
|
||||
if not ramble.exists():
|
||||
return None
|
||||
args = [sys.executable, str(ramble),
|
||||
"--provider", provider,
|
||||
"--claude-cmd", claude_cmd,
|
||||
"--prompt", "Describe your feature idea in your own words",
|
||||
"--fields"] + field_labels + [
|
||||
"--criteria", json.dumps({
|
||||
"Title": "camelCase or kebab-case, <= 32 chars",
|
||||
"Intent": "<= 2 sentences"
|
||||
})
|
||||
]
|
||||
say("[•] Launching Ramble… (submit to return)")
|
||||
cp = subprocess.run(args, text=True, capture_output=True, cwd=repo_root)
|
||||
if cp.stderr and cp.stderr.strip():
|
||||
say("[ramble stderr]\n" + cp.stderr.strip())
|
||||
try:
|
||||
data = json.loads((cp.stdout or "").strip())
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
# Normalize: accept either {"fields":{...}} or flat {"Title":...}
|
||||
fields = data.get("fields") if isinstance(data, dict) else None
|
||||
if not isinstance(fields, dict):
|
||||
fields = {k: data.get(k, "") for k in field_labels}
|
||||
return {k: (fields.get(k) or "").strip() for k in field_labels}
|
||||
|
||||
def render_request_from_template(tmpl: str, fields: Dict[str, str], fid: str, created: str) -> str:
|
||||
# if template has <title>, replace; also replace known placeholders if present
|
||||
body = tmpl
|
||||
replacements = {
|
||||
"<title>": fields.get("Title", ""),
|
||||
"<one paragraph describing purpose>": fields.get("Intent", ""),
|
||||
"<why this is needed now>": fields.get("Motivation / Problem", ""),
|
||||
"<bulleted list of limitations>": fields.get("Constraints / Non-Goals", ""),
|
||||
"<short implementation outline>": fields.get("Rough Proposal", ""),
|
||||
"<bulleted list of uncertainties>": fields.get("Open Questions", ""),
|
||||
"<name>": fields.get("Author", ""),
|
||||
}
|
||||
for needle, val in replacements.items():
|
||||
body = body.replace(needle, val)
|
||||
|
||||
# Append meta block if not already present
|
||||
if "Feature ID" not in body or "Meta" not in body:
|
||||
meta = f"""
|
||||
**Feature ID**: {fid}
|
||||
**Meta**: Created: {created} • Author: {fields.get('Author','').strip() or '—'}
|
||||
""".lstrip()
|
||||
body = body.strip() + "\n\n" + meta
|
||||
return body.strip() + "\n"
|
||||
|
||||
def seed_discussion_files(dir_disc: Path, fid: str, created: str) -> None:
|
||||
req = f"""---
|
||||
type: discussion
|
||||
stage: feature
|
||||
status: OPEN
|
||||
feature_id: {fid}
|
||||
created: {created}
|
||||
---
|
||||
## Summary
|
||||
Initial discussion for feature `{fid}`. Append your comments below.
|
||||
|
||||
## Participation
|
||||
- Maintainer: Kickoff. VOTE: READY
|
||||
"""
|
||||
write_text(dir_disc / "feature.feature.discussion.md", req)
|
||||
|
||||
sum_md = f"""# 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)
|
||||
- {created} Maintainer: Kickoff
|
||||
<!-- SUMMARY:TIMELINE END -->
|
||||
|
||||
<!-- SUMMARY:LINKS START -->
|
||||
## Links
|
||||
- Design/Plan: ../design/design.md
|
||||
<!-- SUMMARY:LINKS END -->
|
||||
"""
|
||||
write_text(dir_disc / "feature.discussion.sum.md", sum_md)
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--dir", help="Repo root (defaults to git root or CWD)")
|
||||
ap.add_argument("--title", help="Feature title (useful without Ramble)")
|
||||
ap.add_argument("--no-ramble", action="store_true", help="Disable Ramble UI")
|
||||
ap.add_argument("--provider", choices=["mock", "claude"], default="mock")
|
||||
ap.add_argument("--claude-cmd", default="claude")
|
||||
args = ap.parse_args()
|
||||
|
||||
start = Path(args.dir).expanduser().resolve() if args.dir else Path.cwd()
|
||||
repo = git_root_or_cwd(start)
|
||||
say(f"[=] Using repository: {repo}")
|
||||
|
||||
tmpl_path = repo / "process" / "templates" / "feature_request.md"
|
||||
tmpl = read_text(tmpl_path)
|
||||
parsed_fields = find_template_fields(tmpl) or [(f, "") for f in default_fields()]
|
||||
field_labels = [name for (name, _) in parsed_fields]
|
||||
if "Title" not in field_labels:
|
||||
field_labels = ["Title"] + field_labels
|
||||
|
||||
# Try Ramble unless disabled
|
||||
fields: Dict[str, str] | None = None
|
||||
if not args.no_ramble:
|
||||
fields = try_ramble(repo, field_labels, provider=args.provider, claude_cmd=args.claude_cmd)
|
||||
|
||||
# Terminal prompts fallback
|
||||
if not fields:
|
||||
fields = collect_via_prompts(field_labels)
|
||||
if args.title:
|
||||
fields["Title"] = args.title
|
||||
|
||||
# Derive slug & feature id
|
||||
slug = slugify(fields.get("Title", "") or args.title or "feature")
|
||||
fid = f"FR_{today()}_{slug}"
|
||||
|
||||
# Build target paths
|
||||
fr_dir = repo / "Docs" / "features" / fid
|
||||
disc_dir = fr_dir / "discussions"
|
||||
fr_dir.mkdir(parents=True, exist_ok=True)
|
||||
disc_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Render request.md
|
||||
if tmpl:
|
||||
body = render_request_from_template(tmpl, fields, fid=fid, created=today())
|
||||
else:
|
||||
# fallback body
|
||||
body = f"""# Feature Request: {fields.get('Title','')}
|
||||
|
||||
**Intent**: {fields.get('Intent','')}
|
||||
**Motivation / Problem**: {fields.get('Motivation / Problem','')}
|
||||
**Constraints / Non-Goals**:
|
||||
{fields.get('Constraints / Non-Goals','')}
|
||||
**Rough Proposal**:
|
||||
{fields.get('Rough Proposal','')}
|
||||
**Open Questions**:
|
||||
{fields.get('Open Questions','')}
|
||||
|
||||
**Feature ID**: {fid}
|
||||
**Meta**: Created: {today()} • Author: {fields.get('Author','')}
|
||||
"""
|
||||
write_text(fr_dir / "request.md", body)
|
||||
|
||||
# Seed discussion & summary
|
||||
seed_discussion_files(disc_dir, fid=fid, created=today())
|
||||
|
||||
say(f"[✓] Created feature at: {fr_dir}")
|
||||
say("Next:")
|
||||
say(f" git add {fr_dir}")
|
||||
say(f" git commit -m \"feat: start {fid}\"")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -699,6 +699,7 @@ def parse_args():
|
|||
p.add_argument("--prompt", default="Explain your new feature idea")
|
||||
p.add_argument("--fields", nargs="+", default=["Summary","Title","Intent","ProblemItSolves","BriefOverview"])
|
||||
p.add_argument("--criteria", default="", help="JSON mapping of field -> criteria")
|
||||
p.add_argument("--hints", default="", help="JSON list of hint strings")
|
||||
p.add_argument("--timeout", type=int, default=90)
|
||||
p.add_argument("--tail", type=int, default=6000)
|
||||
p.add_argument("--debug", action="store_true")
|
||||
|
|
@ -733,11 +734,23 @@ if __name__ == "__main__":
|
|||
print("[FATAL] 'requests' is required for image backends. pip install requests", file=sys.stderr)
|
||||
sys.exit(3)
|
||||
|
||||
# Parse JSON args (tolerate empty/invalid)
|
||||
try:
|
||||
criteria = json.loads(args.criteria) if args.criteria else {}
|
||||
if not isinstance(criteria, dict): criteria = {}
|
||||
except Exception:
|
||||
criteria = {}
|
||||
try:
|
||||
hints = json.loads(args.hints) if args.hints else None
|
||||
if hints is not None and not isinstance(hints, list): hints = None
|
||||
except Exception:
|
||||
hints = None
|
||||
|
||||
demo = open_ramble_dialog(
|
||||
prompt=args.prompt,
|
||||
fields=args.fields,
|
||||
field_criteria=criteria,
|
||||
hints=None,
|
||||
hints = hints,
|
||||
provider=provider,
|
||||
enable_stability=args.stability,
|
||||
enable_pexels=args.pexels,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
# Project Guide
|
||||
|
||||
## Core idea
|
||||
- An **empty project’s first feature defines the whole project**.
|
||||
Subsequent features extend that foundation.
|
||||
|
||||
## Daily flow
|
||||
1. Create or update features under `Docs/features/FR_*/...`.
|
||||
2. Commit. The pre-commit hook **drives the discussion** and **maintains summaries** (within marker blocks).
|
||||
3. Discuss in `Docs/features/.../discussions/*.discussion.md` and **end each comment with**
|
||||
`VOTE: READY` or `CHANGES` or `REJECT`.
|
||||
|
||||
## First run
|
||||
- After installation, make an initial commit to activate the hook:
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "chore: initial commit"
|
||||
```
|
||||
## Start a new feature (recommended)
|
||||
- Copy process/templates/feature_request.md to Docs/features/FR_YYYY-MM-DD_<slug>/request.md
|
||||
- Fill in: Intent, Motivation, Constraints, Rough Proposal, Open Questions, Author
|
||||
- Commit; the system will drive the discussion and generate/maintain summaries automatically.
|
||||
|
||||
## Notes
|
||||
- Keep discussions append-only; votes are single-line VOTE: markers.
|
||||
- Human READY is required at Implementation/Release stages.
|
||||
- Ramble (ramble.py) is optional; it can extract fields from your free-form notes.
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
type: discussion
|
||||
stage: <feature|design|implementation|testing|review>
|
||||
status: OPEN
|
||||
feature_id: <FR_...>
|
||||
created: <YYYY-MM-DD>
|
||||
|
||||
promotion_rule:
|
||||
allow_agent_votes: true
|
||||
ready_min_eligible_votes: all
|
||||
reject_min_eligible_votes: all
|
||||
|
||||
participation:
|
||||
instructions: |
|
||||
- Append your input at the end as: "YourName: your comment…"
|
||||
- Every comment must end with a vote line: "VOTE: READY|CHANGES|REJECT"
|
||||
- Agents/bots must prefix names with "AI_"
|
||||
|
||||
voting:
|
||||
values: [READY, CHANGES, REJECT]
|
||||
---
|
||||
## Summary
|
||||
2-4 sentence summary of current state
|
||||
|
||||
## Participation
|
||||
comments appended below
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<!--META
|
||||
{
|
||||
"kind": "discussion",
|
||||
"tokens": ["FeatureId", "CreatedDate"]
|
||||
}
|
||||
-->
|
||||
|
||||
## Summary
|
||||
Initial discussion for {FeatureId}. Append your comments below.
|
||||
|
||||
## Participation
|
||||
- Maintainer: Kickoff. VOTE: READY
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<!--META
|
||||
{
|
||||
"kind": "discussion_summary",
|
||||
"tokens": ["FeatureId", "CreatedDate"]
|
||||
}
|
||||
-->
|
||||
|
||||
# Summary — Feature {FeatureId}
|
||||
|
||||
<!-- 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)
|
||||
- {CreatedDate} Maintainer: Kickoff
|
||||
<!-- SUMMARY:TIMELINE END -->
|
||||
|
||||
<!-- SUMMARY:LINKS START -->
|
||||
## Links
|
||||
- Design/Plan: ../design/design.md
|
||||
<!-- SUMMARY:LINKS END -->
|
||||
|
|
@ -1,10 +1,35 @@
|
|||
# Feature Request: <title>
|
||||
|
||||
**Feature ID**: <FR_YYYY-MM-DD_slug>
|
||||
**Intent**: <one paragraph describing purpose>
|
||||
**Motivation / Problem**: <why this is needed now>
|
||||
**Constraints / Non-Goals**: <bulleted list of limitations>
|
||||
**Rough Proposal**: <short implementation outline>
|
||||
**Open Questions**: <bulleted list of uncertainties>
|
||||
**Meta**: Created: <date> • Author: <name>
|
||||
Discussion Template (process/templates/discussion.md):
|
||||
<!--META
|
||||
{
|
||||
"kind": "feature_request",
|
||||
"ramble_fields": [
|
||||
{"name": "Title", "hint": "camelCase, ≤24 chars", "default": "initialProjectDesign"},
|
||||
{"name": "Intent"},
|
||||
{"name": "ProblemItSolves"},
|
||||
{"name": "BriefOverview"},
|
||||
{"name": "Summary", "hint": "≤2 sentences"}
|
||||
],
|
||||
"criteria": {
|
||||
"Title": "camelCase, <= 24 chars",
|
||||
"Summary": "<= 2 sentences"
|
||||
},
|
||||
"hints": [
|
||||
"What is it called?",
|
||||
"Who benefits most?",
|
||||
"What problem does it solve?",
|
||||
"What does success look like?"
|
||||
],
|
||||
"tokens": ["FeatureId", "CreatedDate", "Title", "Intent", "ProblemItSolves", "BriefOverview", "Summary"]
|
||||
}
|
||||
-->
|
||||
|
||||
# Feature Request: {Title}
|
||||
|
||||
**Intent**: {Intent}
|
||||
**Motivation / Problem**: {ProblemItSolves}
|
||||
**Brief Overview**: {BriefOverview}
|
||||
|
||||
**Summary**: {Summary}
|
||||
|
||||
**Meta**: FeatureId: {FeatureId} • Created: {CreatedDate}
|
||||
|
|
|
|||
|
|
@ -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,27 @@
|
|||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.egg-info/
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# Node (if any JS tooling appears)
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Env / secrets
|
||||
.env
|
||||
.env.*
|
||||
secrets/
|
||||
|
||||
# OS/editor
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Project
|
||||
.git/ai-rules-*
|
||||
|
|
@ -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.
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# Cascading Development - AI–Human Collaboration System
|
||||
# CascadingDev - AI–Human Collaboration System
|
||||
## Process & Architecture Design Document (v2.0)
|
||||
- Feature ID: FR_2025-10-21_initial-feature-request
|
||||
- Status: Design Approved (Ready for Implementation)
|
||||
|
|
@ -29,6 +29,8 @@
|
|||
|
||||
We are implementing a **Git-native**, rules-driven workflow that enables seamless collaboration between humans and multiple AI agents across the entire software development lifecycle. The system uses cascading .ai-rules.yml configurations and a thin Bash pre-commit hook to automatically generate and maintain development artifacts (discussions, design docs, reviews, diagrams, plans). A Python orchestrator provides structured checks and status reporting while preserving the fast Bash execution path.
|
||||
|
||||
**Scope clarification:** The document you are reading is the *CascadingDev system* design. It is **not** copied into user projects. End-users get a short `USER_GUIDE.md` and a `create_feature.py` tool; their **first feature request defines the project**, and its later design doc belongs to that project, not to CascadingDev.
|
||||
|
||||
> *Git-Native Philosophy: Every conversation, decision, and generated artifact lives in the same version-controlled environment as the source code. There are no external databases, dashboards, or SaaS dependencies required for the core workflow.
|
||||
|
||||
### Objective:
|
||||
|
|
@ -57,122 +59,198 @@ Human → Git Commit → Pre-commit Hook → AI Generator → Markdown Artifact
|
|||
Orchestrator ← Discussion Summaries ← AI Moderator
|
||||
```
|
||||
|
||||
## Repository Layout
|
||||
## Repository Layouts
|
||||
|
||||
This section clarifies three different directory structures that are easy to confuse:
|
||||
|
||||
### Terminology
|
||||
- **CascadingDev Repo** — The tooling project (this repository) that builds installers
|
||||
- **Install Bundle** — The distributable artifact created by `tools/build_installer.py`
|
||||
- **User Project** — A new repository scaffolded when a user runs the installer
|
||||
|
||||
---
|
||||
|
||||
### A) CascadingDev Repository (Tooling Source)
|
||||
|
||||
This is the development repository where CascadingDev itself is maintained.
|
||||
|
||||
### Canonical Structure (Per-Feature Folders)
|
||||
```text
|
||||
/ (repository root)
|
||||
├─ .ai-rules.yml # Global defaults + file associations
|
||||
├─ automation/ # Orchestrator & adapters
|
||||
│ ├─ workflow.py # Python status/reporting (v1 non-blocking)
|
||||
CascadingDev/ # This repository
|
||||
├─ src/cascadingdev/ # Core Python modules
|
||||
│ ├─ cli.py # Developer CLI (cdev command)
|
||||
│ ├─ setup_project.py # Installer script (copied to bundle)
|
||||
│ ├─ utils.py # Version management, utilities
|
||||
│ ├─ feature_seed.py # Feature scaffolding logic
|
||||
│ ├─ rules_seed.py # Rules seeding logic
|
||||
│ ├─ fs_scaffold.py # Filesystem utilities
|
||||
│ └─ ramble_integration.py # Ramble GUI integration
|
||||
├─ assets/ # Single source of truth for shipped files
|
||||
│ ├─ hooks/
|
||||
│ │ └─ pre-commit # Git hook template (bash script)
|
||||
│ ├─ templates/ # Templates copied to user projects
|
||||
│ │ ├─ USER_GUIDE.md # Daily usage guide
|
||||
│ │ ├─ feature_request.md # Feature request template
|
||||
│ │ ├─ feature.discussion.md # Discussion template
|
||||
│ │ ├─ feature.discussion.sum.md # Summary template
|
||||
│ │ ├─ design_doc.md # Design document template
|
||||
│ │ ├─ root_gitignore # Root .gitignore template
|
||||
│ │ ├─ process/
|
||||
│ │ │ └─ policies.yml # Machine-readable policies
|
||||
│ │ └─ rules/
|
||||
│ │ ├─ root.ai-rules.yml # Root cascading rules
|
||||
│ │ └─ features.ai-rules.yml # Feature-level rules
|
||||
│ └─ runtime/ # Scripts copied to bundle & user projects
|
||||
│ ├─ ramble.py # GUI for feature creation (PySide6/PyQt5)
|
||||
│ ├─ create_feature.py # CLI for feature creation
|
||||
│ └─ .gitignore.seed # Gitignore seed patterns
|
||||
├─ tools/ # Build and test automation
|
||||
│ ├─ build_installer.py # Creates install bundle
|
||||
│ ├─ smoke_test.py # Basic validation tests
|
||||
│ └─ bundle_smoke.py # End-to-end installer testing
|
||||
├─ install/ # Build output directory (git-ignored)
|
||||
│ └─ cascadingdev-<version>/ # Generated installer bundle (see section B)
|
||||
├─ docs/ # System documentation
|
||||
│ ├─ DESIGN.md # This comprehensive design document
|
||||
│ └─ INSTALL.md # Installation instructions
|
||||
├─ tests/ # Test suite (planned, not yet implemented)
|
||||
│ ├─ unit/
|
||||
│ ├─ integration/
|
||||
│ └─ bin/
|
||||
├─ VERSION # Semantic version (e.g., 0.1.0)
|
||||
├─ pyproject.toml # Python package configuration
|
||||
├─ README.md # Public-facing project overview
|
||||
└─ CLAUDE.md # AI assistant guidance
|
||||
|
||||
FUTURE (planned but not yet implemented):
|
||||
├─ automation/ # 🚧 M1: Orchestration layer
|
||||
│ ├─ workflow.py # Status reporting, vote parsing
|
||||
│ ├─ adapters/
|
||||
│ │ ├─ claude_adapter.py # Model interface (future)
|
||||
│ │ ├─ gitea_adapter.py # Gitea API integration (future)
|
||||
│ │ └─ agent_coordinator.py # Role routing & task allocation (future)
|
||||
│ ├─ 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/
|
||||
│ ├─ feature_request.md
|
||||
│ ├─ discussion.md
|
||||
│ ├─ design_doc.md
|
||||
│ └─ implementation_plan.md
|
||||
├─ Docs/
|
||||
│ ├─ features/
|
||||
│ │ ├─ .ai-rules.yml # Folder-scoped rules for all features
|
||||
│ │ ├─ FR_YYYY-MM-DD_<slug>/ # Individual feature folders
|
||||
│ │ │ ├─ request.md # Original feature request
|
||||
│ │ │ ├─ discussions/ # Stage-specific conversations
|
||||
│ │ │ │ ├─ feature.discussion.md # Discuss the request
|
||||
│ │ │ │ ├─ feature.discussion.sum.md # Summary of the request discussion
|
||||
│ │ │ │ ├─ design.discussion.md # Discuss the design
|
||||
│ │ │ │ ├─ design.discussion.sum.md # Summary of the design discussion
|
||||
│ │ │ │ ├─ implementation.discussion.md # Track implementation
|
||||
│ │ │ │ ├─ implementation.discussion.sum.md # Summary of the implementation discussion
|
||||
│ │ │ │ ├─ testing.discussion.md # Plan/track testing
|
||||
│ │ │ │ ├─ testing.discussion.sum.md # Summary of the testing discussion
|
||||
│ │ │ │ ├─ review.discussion.md # Final review
|
||||
│ │ │ │ └─ review.discussion.sum.md # Summary of the review discussion
|
||||
│ │ │ ├─ design/ # Design artifacts
|
||||
│ │ │ │ ├─ design.md # Evolving design document
|
||||
│ │ │ │ └─ diagrams/ # Architecture diagrams
|
||||
│ │ │ ├─ implementation/ # Implementation artifacts
|
||||
│ │ │ │ ├─ plan.md # Implementation plan
|
||||
│ │ │ │ └─ tasks.md # Task checklist
|
||||
│ │ │ ├─ testing/ # Testing artifacts
|
||||
│ │ │ │ ├─ testplan.md # Test strategy
|
||||
│ │ │ │ └─ checklist.md # Test checklist
|
||||
│ │ │ ├─ review/ # Review artifacts
|
||||
│ │ │ │ └─ findings.md # Feature-specific review findings
|
||||
│ │ │ └─ bugs/ # Auto-generated bug reports
|
||||
│ │ │ └─ BUG_YYYYMMDD_<slug>/
|
||||
│ │ │ ├─ report.md
|
||||
│ │ │ ├─ discussion.md
|
||||
│ │ │ └─ fix/
|
||||
│ │ │ ├─ plan.md
|
||||
│ │ │ └─ tasks.md
|
||||
│ ├─ discussions/
|
||||
│ │ └─ reviews/ # Code reviews from hook
|
||||
│ └─ diagrams/
|
||||
│ └─ file_diagrams/ # PlantUML from source files
|
||||
├─ src/ # Application source code
|
||||
└─ tests/ # System test suite
|
||||
├─ unit/
|
||||
├─ integration/
|
||||
└─ bin/
|
||||
│ │ ├─ claude_adapter.py # AI model integration
|
||||
│ │ └─ gitea_adapter.py # Gitea API integration
|
||||
│ └─ agents.yml # Agent role definitions
|
||||
```
|
||||
|
||||
**Purpose:** Development, testing, and building the installer. The `assets/` directory is the single source of truth for all files shipped to users.
|
||||
|
||||
The sections below describe the meta-infrastructure of CascadingDev itself — how it builds and distributes the installer that generates user projects.
|
||||
---
|
||||
|
||||
### B) Install Bundle (Distribution Artifact)
|
||||
|
||||
This is the self-contained, portable installer created by `tools/build_installer.py`.
|
||||
|
||||
```text
|
||||
cascadingdev-<version>/ # Distributable bundle
|
||||
├─ setup_cascadingdev.py # Installer entry point (stdlib only)
|
||||
├─ ramble.py # GUI for first feature (optional)
|
||||
├─ create_feature.py # CLI tool for creating features
|
||||
├─ assets/ # Embedded resources
|
||||
│ ├─ hooks/
|
||||
│ │ └─ pre-commit # Pre-commit hook template
|
||||
│ └─ templates/ # All templates from source assets/
|
||||
│ ├─ USER_GUIDE.md
|
||||
│ ├─ feature_request.md
|
||||
│ ├─ feature.discussion.md
|
||||
│ ├─ feature.discussion.sum.md
|
||||
│ ├─ design_doc.md
|
||||
│ ├─ root_gitignore
|
||||
│ ├─ process/
|
||||
│ │ └─ policies.yml
|
||||
│ └─ rules/
|
||||
│ ├─ root.ai-rules.yml
|
||||
│ └─ features.ai-rules.yml
|
||||
├─ INSTALL.md # Bundle-local instructions
|
||||
└─ VERSION # Version metadata
|
||||
|
||||
```
|
||||
|
||||
**Purpose:** End-user distribution. Can be zipped and shared. Requires only Python 3.10+ stdlib (PySide6 optional for GUI).
|
||||
|
||||
**Rationale:** Minimal, auditable, portable. No external dependencies for core functionality. Users can inspect all files before running.
|
||||
|
||||
---
|
||||
|
||||
### C) User Project (Generated by Installer)
|
||||
|
||||
This is the structure created when a user runs `setup_cascadingdev.py --target /path/to/project`.
|
||||
|
||||
```text
|
||||
my-project/ # User's application repository
|
||||
├─ .git/ # Git repository
|
||||
│ └─ hooks/
|
||||
│ └─ pre-commit # Installed automatically from bundle
|
||||
├─ .gitignore # Generated from root_gitignore template
|
||||
├─ .ai-rules.yml # Root cascading rules (from templates/rules/)
|
||||
├─ USER_GUIDE.md # Daily workflow reference
|
||||
├─ ramble.py # Copied from bundle (optional GUI helper)
|
||||
├─ create_feature.py # Copied from bundle (CLI tool)
|
||||
├─ Docs/ # Documentation and feature tracking
|
||||
│ ├─ features/ # All features live here
|
||||
│ │ ├─ .ai-rules.yml # Feature-level cascading rules
|
||||
│ │ └─ FR_YYYY-MM-DD_<slug>/ # Individual feature folders
|
||||
│ │ ├─ request.md # Original feature request
|
||||
│ │ └─ discussions/ # Stage-specific conversation threads
|
||||
│ │ ├─ feature.discussion.md # Feature discussion
|
||||
│ │ ├─ feature.discussion.sum.md # Auto-maintained summary
|
||||
│ │ ├─ design.discussion.md # Design discussion
|
||||
│ │ ├─ design.discussion.sum.md # Auto-maintained summary
|
||||
│ │ ├─ implementation.discussion.md # Implementation tracking
|
||||
│ │ ├─ implementation.discussion.sum.md
|
||||
│ │ ├─ testing.discussion.md # Test planning
|
||||
│ │ ├─ testing.discussion.sum.md
|
||||
│ │ ├─ review.discussion.md # Final review
|
||||
│ │ └─ review.discussion.sum.md
|
||||
│ │ ├─ design/ # Design artifacts (created during design stage)
|
||||
│ │ │ ├─ design.md # Evolving design document
|
||||
│ │ │ └─ diagrams/ # Architecture diagrams
|
||||
│ │ ├─ implementation/ # Implementation artifacts
|
||||
│ │ │ ├─ plan.md # Implementation plan
|
||||
│ │ │ └─ tasks.md # Task checklist
|
||||
│ │ ├─ testing/ # Testing artifacts
|
||||
│ │ │ ├─ testplan.md # Test strategy
|
||||
│ │ │ └─ checklist.md # Test checklist
|
||||
│ │ ├─ review/ # Review artifacts
|
||||
│ │ │ └─ findings.md # Code review findings
|
||||
│ │ └─ bugs/ # Bug sub-cycles (future)
|
||||
│ │ └─ BUG_YYYYMMDD_<slug>/
|
||||
│ │ ├─ report.md
|
||||
│ │ ├─ discussion.md
|
||||
│ │ └─ fix/
|
||||
│ │ ├─ plan.md
|
||||
│ │ └─ tasks.md
|
||||
│ ├─ discussions/ # Global discussions (future)
|
||||
│ │ └─ reviews/ # Code reviews from hook
|
||||
│ └─ diagrams/ # Auto-generated diagrams (future)
|
||||
│ └─ file_diagrams/ # PlantUML from source files
|
||||
├─ process/ # Process configuration
|
||||
│ ├─ policies.yml # Machine-readable policies (voting, gates)
|
||||
│ └─ templates/ # Local template overrides (optional)
|
||||
├─ src/ # User's application source code
|
||||
│ └─ (user's code)
|
||||
└─ tests/ # User's test suite
|
||||
├─ unit/
|
||||
└─ integration/
|
||||
|
||||
FUTURE (not currently created, planned for M1+):
|
||||
├─ automation/ # 🚧 M1: Orchestration layer
|
||||
│ ├─ workflow.py # Vote parsing, status reporting
|
||||
│ ├─ adapters/ # Model and platform integrations
|
||||
│ └─ agents.yml # Agent role configuration
|
||||
```
|
||||
|
||||
**Purpose:** This is the user's actual project repository where they develop their application while using the CascadingDev workflow.
|
||||
|
||||
**Key Points:**
|
||||
- The first feature request defines the entire project's purpose
|
||||
- All discussions are version-controlled alongside code
|
||||
- Pre-commit hook maintains summary files automatically
|
||||
- Templates can be overridden locally in `process/templates/`
|
||||
- The `automation/` directory is planned but not yet implemented (M1)
|
||||
|
||||
---
|
||||
|
||||
## Installation & Distribution Architecture
|
||||
|
||||
### Terminology (clarified)
|
||||
- **CascadingDev** — this tooling project (the code in this repository).
|
||||
- **User’s project** — a new repository scaffolded by running CascadingDev’s installer.
|
||||
- **Install bundle** — the small, distributable folder produced by the build process (unzipped and executed by end users).
|
||||
|
||||
|
||||
### Repository Layout (authoritative)
|
||||
|
||||
Note: This section refers to the CascadingDev repository itself. For the structure of a user’s generated project, see “First-Run Flow” below.
|
||||
```text
|
||||
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}
|
||||
│ └─ runtime/ramble.py
|
||||
├─ tools/build_installer.py # creates install/cascadingdev-<version>/
|
||||
├─ install/ # build output (git-ignored)
|
||||
│ └─ cascadingdev-<version>/ # unzip + run bundle (setup_cascadingdev.py inside)
|
||||
├─ VERSION # semantic version of CascadingDev
|
||||
├─ DESIGN.md, README.md, docs/, tests/
|
||||
```
|
||||
|
||||
**Why:** All runtime assets live once under `assets/`.
|
||||
The builder copies only what end users need into a clean bundle.
|
||||
Development happens in `src/` and is testable; distribution is “unzip + run”.
|
||||
|
||||
|
||||
### Install Bundle Specification
|
||||
|
||||
Contents of `install/cascadingdev-<version>/`:
|
||||
|
||||
- `setup_cascadingdev.py` — single-file installer (stdlib-only)
|
||||
- `DESIGN.md` — copied for user reference
|
||||
- `ramble.py` — GUI dialog for first feature request (PySide6/PyQt5)
|
||||
- `assets/hooks/pre-commit` — git pre-commit template (executable)
|
||||
- `assets/templates/*.md` — feature/discussion/design templates
|
||||
- `VERSION` — set from repo root `VERSION`
|
||||
|
||||
**Rationale:** Minimal, auditable, portable; no local package imports required.
|
||||
|
||||
|
||||
### First-Run Flow (User’s Project Initialization)
|
||||
### First-Run Flow (User's Project Initialization)
|
||||
|
||||
User runs:
|
||||
```bash
|
||||
|
|
@ -180,10 +258,12 @@ python setup_cascadingdev.py --target /path/to/users-project [--no-ramble] [--pr
|
|||
```
|
||||
|
||||
**Installer actions:**
|
||||
- Creates standard folders (`Docs/`, `process/templates/`, etc.)
|
||||
- Copies templates, `ramble.py`, `DESIGN.md`, and installs pre-commit hook
|
||||
- Creates standard folders (Docs/, process/templates/, etc.)
|
||||
- Copies templates, ramble.py, and create_feature.py into the user project
|
||||
- Initializes git (main branch), writes `.gitignore`
|
||||
- Launches Ramble (unless `--no-ramble`) to collect the first Feature Request
|
||||
- Installs pre-commit hook
|
||||
- Optionally launches Ramble (unless --no-ramble) to help collect first Feature Request
|
||||
- Writes a concise USER_GUIDE.md into the user project root for day-to-day use
|
||||
|
||||
**Seeds:**
|
||||
```
|
||||
|
|
@ -196,6 +276,7 @@ Initial commit message: “bootstrap Cascading Development scaffolding”.
|
|||
|
||||
**Fallback:** If Ramble JSON isn’t returned, installer prints to stderr and optionally falls back to terminal prompts.
|
||||
|
||||
Important: The CascadingDev DESIGN.md is not copied into user projects. The first feature’s design doc (created later at the design stage) becomes the project’s own design document.
|
||||
|
||||
### Pre-Commit Hook (v1 behavior)
|
||||
- Fast regex secret scan on staged diffs
|
||||
|
|
@ -204,11 +285,452 @@ Initial commit message: “bootstrap Cascading Development scaffolding”.
|
|||
|
||||
Policy: v1 is non-blocking; blocking checks are introduced gradually in later versions.
|
||||
|
||||
---
|
||||
|
||||
## Template META System & Ramble Integration
|
||||
|
||||
### Overview
|
||||
|
||||
CascadingDev includes a sophisticated **template metadata system** that allows templates to be self-describing. This enables dynamic GUI generation, field validation, and flexible template rendering without hardcoding form structures in the installer.
|
||||
|
||||
**Status**: ✅ **Fully implemented** (v0.1.0)
|
||||
|
||||
### Template META Format
|
||||
|
||||
Templates can include JSON metadata inside HTML comments at the top of the file:
|
||||
|
||||
```markdown
|
||||
<!--META
|
||||
{
|
||||
"kind": "feature_request",
|
||||
"ramble_fields": [
|
||||
{"name": "Title", "hint": "camelCase, ≤24 chars", "default": "initialProjectDesign"},
|
||||
{"name": "Intent"},
|
||||
{"name": "Summary", "hint": "≤2 sentences"}
|
||||
],
|
||||
"criteria": {
|
||||
"Title": "camelCase, <= 24 chars",
|
||||
"Summary": "<= 2 sentences"
|
||||
},
|
||||
"hints": [
|
||||
"What is it called?",
|
||||
"Who benefits most?",
|
||||
"What problem does it solve?"
|
||||
],
|
||||
"tokens": ["FeatureId", "CreatedDate", "Title", "Intent", "Summary"]
|
||||
}
|
||||
-->
|
||||
|
||||
# Feature Request: {Title}
|
||||
|
||||
**Intent**: {Intent}
|
||||
**Summary**: {Summary}
|
||||
|
||||
**Meta**: FeatureId: {FeatureId} • Created: {CreatedDate}
|
||||
```
|
||||
|
||||
### META Fields Reference
|
||||
|
||||
| Field | Type | Purpose | Example |
|
||||
|-------|------|---------|---------|
|
||||
| `kind` | string | Template type identifier | `"feature_request"` |
|
||||
| `ramble_fields` | array | Field definitions for Ramble GUI | See below |
|
||||
| `criteria` | object | Validation rules per field | `{"Title": "camelCase, <= 24 chars"}` |
|
||||
| `hints` | array | User guidance prompts | `["What is it called?"]` |
|
||||
| `tokens` | array | List of available placeholder tokens | `["FeatureId", "Title"]` |
|
||||
|
||||
### ramble_fields Specification
|
||||
|
||||
Each field in `ramble_fields` is an object with:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "FieldName", // Required: field identifier
|
||||
"hint": "display hint", // Optional: shown to user as guidance
|
||||
"default": "defaultValue" // Optional: pre-filled value
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
"ramble_fields": [
|
||||
{"name": "Title", "hint": "camelCase, ≤24 chars", "default": "initialProjectDesign"},
|
||||
{"name": "Intent"},
|
||||
{"name": "ProblemItSolves"},
|
||||
{"name": "BriefOverview"},
|
||||
{"name": "Summary", "hint": "≤2 sentences"}
|
||||
]
|
||||
```
|
||||
|
||||
### How META is Processed
|
||||
|
||||
**In `setup_project.py`** (`src/cascadingdev/setup_project.py:64-115`):
|
||||
|
||||
1. **Parsing** (`load_template_with_meta()`):
|
||||
```python
|
||||
meta, body = load_template_with_meta(template_path)
|
||||
# meta = {"ramble_fields": [...], "criteria": {...}, ...}
|
||||
# body = template text without META comment
|
||||
```
|
||||
|
||||
2. **Extraction** (`meta_ramble_config()`):
|
||||
```python
|
||||
fields, defaults, criteria, hints = meta_ramble_config(meta)
|
||||
# fields = ["Title", "Intent", "Summary", ...]
|
||||
# defaults = {"Title": "initialProjectDesign"}
|
||||
# criteria = {"Title": "camelCase, <= 24 chars"}
|
||||
# hints = ["What is it called?", ...]
|
||||
```
|
||||
|
||||
3. **Rendering** (`render_placeholders()`):
|
||||
```python
|
||||
values = {"Title": "myFeature", "FeatureId": "FR_2025-10-30_...", ...}
|
||||
rendered = render_placeholders(body, values)
|
||||
# Replaces {Token} and {{Token}} with actual values
|
||||
```
|
||||
|
||||
### Token Replacement Rules
|
||||
|
||||
The `render_placeholders()` function supports two-pass replacement:
|
||||
|
||||
1. **First pass**: Replace `{{Token}}` (double braces) - for tokens that shouldn't be re-processed
|
||||
2. **Second pass**: Replace `{Token}` (single braces) using Python's `.format_map()`
|
||||
|
||||
**System-provided tokens:**
|
||||
- `{FeatureId}` - Generated feature ID (e.g., `FR_2025-10-30_initial-feature-request`)
|
||||
- `{CreatedDate}` - Current date in `YYYY-MM-DD` format
|
||||
- `{Title}`, `{Intent}`, etc. - User-provided field values
|
||||
|
||||
---
|
||||
|
||||
## Ramble: AI-Powered Feature Capture GUI
|
||||
|
||||
### Overview
|
||||
|
||||
**Ramble** is a sophisticated PySide6/PyQt5 GUI application that helps users articulate feature requests through AI-assisted structured input. It supports multiple AI providers, generates PlantUML diagrams, and returns validated JSON output.
|
||||
|
||||
**Status**: ✅ **Fully implemented** (v0.1.0)
|
||||
|
||||
**Location**: `assets/runtime/ramble.py` (copied to user projects and install bundle)
|
||||
|
||||
### Key Features
|
||||
|
||||
1. **Multi-Provider Architecture** - Pluggable AI backends
|
||||
2. **Dynamic Field Generation** - Driven by template META
|
||||
3. **Field Locking** - Lock fields to preserve context across regenerations
|
||||
4. **PlantUML Integration** - Auto-generate and render architecture diagrams
|
||||
5. **Validation Criteria** - Per-field rules from template metadata
|
||||
6. **Graceful Fallback** - Terminal prompts if GUI fails
|
||||
|
||||
### Supported Providers
|
||||
|
||||
| Provider | Status | Description | Usage |
|
||||
|----------|--------|-------------|-------|
|
||||
| **mock** | ✅ Stable | No external calls, derives fields from ramble text | Default, no setup required |
|
||||
| **claude** | ✅ Stable | Claude CLI integration via subprocess | Requires `claude` CLI in PATH |
|
||||
|
||||
**Provider Selection:**
|
||||
```bash
|
||||
# Mock provider (no AI, instant)
|
||||
python ramble.py --provider mock --fields Title Summary
|
||||
|
||||
# Claude CLI provider
|
||||
python ramble.py --provider claude \
|
||||
--claude-cmd /path/to/claude \
|
||||
--fields Title Summary Intent
|
||||
```
|
||||
|
||||
### Provider Protocol
|
||||
|
||||
All providers implement the `RambleProvider` protocol:
|
||||
|
||||
```python
|
||||
class RambleProvider(Protocol):
|
||||
def generate(
|
||||
self,
|
||||
*,
|
||||
prompt: str, # User's base prompt
|
||||
ramble_text: str, # User's freeform notes
|
||||
fields: List[str], # Required field names
|
||||
field_criteria: Dict[str, str], # Validation rules per field
|
||||
locked_context: Dict[str, str], # Previously locked field values
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Returns:
|
||||
{
|
||||
"summary": str,
|
||||
"fields": Dict[str, str],
|
||||
"uml_blocks": List[Tuple[str, Optional[bytes]]],
|
||||
"image_descriptions": List[str]
|
||||
}
|
||||
"""
|
||||
...
|
||||
```
|
||||
|
||||
### Mock Provider
|
||||
|
||||
**Purpose**: Fast, deterministic testing and offline use.
|
||||
|
||||
**Behavior**:
|
||||
- Derives summary from last 25 words of ramble text
|
||||
- Creates placeholder fields with word count
|
||||
- Generates simple actor-system UML diagram
|
||||
- Returns generic image descriptions
|
||||
|
||||
**Example Output**:
|
||||
```python
|
||||
{
|
||||
"summary": "User wants to track metrics and export them.",
|
||||
"fields": {
|
||||
"Title": "Title: Derived from ramble (42 words). [criteria: camelCase, <=24 chars]",
|
||||
"Intent": "Intent: Derived from ramble (42 words).",
|
||||
},
|
||||
"uml_blocks": [("@startuml\nactor User\n...\n@enduml", None)],
|
||||
"image_descriptions": ["Illustrate the core actor..."]
|
||||
}
|
||||
```
|
||||
|
||||
### Claude CLI Provider
|
||||
|
||||
**Purpose**: Production-quality AI-generated structured output.
|
||||
|
||||
**Setup Requirements**:
|
||||
```bash
|
||||
# Install Claude CLI (npm)
|
||||
npm install -g @anthropics/claude-cli
|
||||
|
||||
# Or provide custom path
|
||||
python ramble.py --provider claude --claude-cmd /custom/path/to/claude
|
||||
```
|
||||
|
||||
**Features**:
|
||||
- Spawns `claude` subprocess with structured prompt
|
||||
- Includes locked field context in prompt
|
||||
- Enforces per-field criteria
|
||||
- Extracts PlantUML blocks from response
|
||||
- Timeout protection (default 120s)
|
||||
- Debug logging to `/tmp/ramble_claude.log`
|
||||
|
||||
**Constructor Options**:
|
||||
```python
|
||||
ClaudeCLIProvider(
|
||||
cmd="claude", # Command name or path
|
||||
extra_args=[], # Additional CLI args
|
||||
timeout_s=120, # Subprocess timeout
|
||||
tail_chars=8000, # Max response length
|
||||
use_arg_p=True, # Use -p flag for prompt
|
||||
debug=False, # Enable debug logging
|
||||
log_path="/tmp/ramble_claude.log"
|
||||
)
|
||||
```
|
||||
|
||||
**Prompt Structure**:
|
||||
The provider builds a comprehensive prompt including:
|
||||
1. User's base prompt
|
||||
2. Locked field context (from previously locked fields)
|
||||
3. User's ramble notes
|
||||
4. Required field list with criteria
|
||||
5. PlantUML and image description requests
|
||||
6. JSON output format specification
|
||||
|
||||
### Integration with Installer
|
||||
|
||||
**In `setup_project.py:151-218`** (`run_ramble_and_collect()`):
|
||||
|
||||
```python
|
||||
def run_ramble_and_collect(target: Path, provider: str = "mock", claude_cmd: str = "claude"):
|
||||
# 1. Load template META to get field configuration
|
||||
fr_tmpl = INSTALL_ROOT / "assets" / "templates" / "feature_request.md"
|
||||
meta, _ = load_template_with_meta(fr_tmpl)
|
||||
field_names, _defaults, criteria, hints = meta_ramble_config(meta)
|
||||
|
||||
# 2. Build dynamic Ramble command from META
|
||||
args = [
|
||||
sys.executable, str(ramble),
|
||||
"--provider", provider,
|
||||
"--fields", *field_names, # From template META
|
||||
]
|
||||
|
||||
if criteria:
|
||||
args += ["--criteria", json.dumps(criteria)]
|
||||
if hints:
|
||||
args += ["--hints", json.dumps(hints)]
|
||||
|
||||
# 3. Launch Ramble, capture JSON output
|
||||
proc = subprocess.run(args, capture_output=True, text=True)
|
||||
|
||||
# 4. Parse JSON or fall back to terminal prompts
|
||||
try:
|
||||
return json.loads(proc.stdout)
|
||||
except:
|
||||
# Terminal fallback: collect fields via input()
|
||||
return collect_via_terminal()
|
||||
```
|
||||
|
||||
### Ramble GUI Workflow
|
||||
|
||||
1. **User writes freeform notes** in the "Ramble" text area
|
||||
2. **Clicks "Generate"** → Provider processes ramble text
|
||||
3. **Review generated fields** → Edit as needed
|
||||
4. **Lock important fields** → Prevents overwrite on regenerate
|
||||
5. **Regenerate if needed** → Locked fields feed back as context
|
||||
6. **Review PlantUML diagrams** → Auto-rendered if plantuml CLI available
|
||||
7. **Click "Submit"** → Returns JSON to installer
|
||||
|
||||
**Output Format**:
|
||||
```json
|
||||
{
|
||||
"summary": "One or two sentence summary",
|
||||
"fields": {
|
||||
"Title": "metricsExportFeature",
|
||||
"Intent": "Enable users to track and export usage metrics",
|
||||
"ProblemItSolves": "Currently no way to analyze usage patterns",
|
||||
"BriefOverview": "Add metrics collection and CSV/JSON export",
|
||||
"Summary": "Track usage metrics and export to various formats."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Terminal Fallback
|
||||
|
||||
If Ramble GUI fails (missing PySide6, JSON parse error, etc.), the installer falls back to terminal input:
|
||||
|
||||
```python
|
||||
def ask(label, default=""):
|
||||
try:
|
||||
v = input(f"{label}: ").strip()
|
||||
return v or default
|
||||
except EOFError:
|
||||
return default
|
||||
|
||||
fields = {
|
||||
"Title": ask("Title (camelCase, <=24 chars)", "initialProjectDesign"),
|
||||
"Intent": ask("Intent", "—"),
|
||||
"Summary": ask("One- or two-sentence summary", ""),
|
||||
}
|
||||
```
|
||||
|
||||
### Adding New Providers
|
||||
|
||||
To add a new provider (e.g., `deepseek`, `openai`):
|
||||
|
||||
1. **Create provider class** in `ramble.py`:
|
||||
```python
|
||||
class DeepseekProvider:
|
||||
def generate(self, *, prompt, ramble_text, fields, field_criteria, locked_context):
|
||||
# Call Deepseek API
|
||||
response = call_deepseek_api(...)
|
||||
return {
|
||||
"summary": ...,
|
||||
"fields": {...},
|
||||
"uml_blocks": [...],
|
||||
"image_descriptions": [...]
|
||||
}
|
||||
```
|
||||
|
||||
2. **Register in CLI parser**:
|
||||
```python
|
||||
p.add_argument("--provider",
|
||||
choices=["mock", "claude", "deepseek"], # Add here
|
||||
default="mock")
|
||||
```
|
||||
|
||||
3. **Instantiate in main()**:
|
||||
```python
|
||||
if args.provider == "deepseek":
|
||||
provider = DeepseekProvider(api_key=os.getenv("DEEPSEEK_API_KEY"))
|
||||
```
|
||||
|
||||
### Advanced Features
|
||||
|
||||
**PlantUML Support**:
|
||||
- Ramble extracts `@startuml...@enduml` blocks from provider responses
|
||||
- Auto-renders to PNG if `plantuml` CLI available
|
||||
- Falls back to text display if rendering fails
|
||||
|
||||
**Image Generation** (Optional):
|
||||
- Supports Stability AI and Pexels APIs
|
||||
- Requires API keys via environment variables
|
||||
- Displays images in GUI if generated
|
||||
|
||||
**Field Locking**:
|
||||
- Checkbox next to each field
|
||||
- Locked fields are highlighted and included in next generation prompt
|
||||
- Enables iterative refinement without losing progress
|
||||
|
||||
**Criteria Validation**:
|
||||
- Displayed alongside each field as hints
|
||||
- Passed to AI provider to enforce constraints
|
||||
- No automatic validation (relies on AI compliance)
|
||||
|
||||
### Configuration Examples
|
||||
|
||||
**Basic usage (mock provider)**:
|
||||
```bash
|
||||
python ramble.py \
|
||||
--fields Title Intent Summary \
|
||||
--prompt "Describe your feature idea"
|
||||
```
|
||||
|
||||
**Production usage (Claude)**:
|
||||
```bash
|
||||
python ramble.py \
|
||||
--provider claude \
|
||||
--claude-cmd ~/.npm-global/bin/claude \
|
||||
--fields Title Intent ProblemItSolves BriefOverview Summary \
|
||||
--criteria '{"Title":"camelCase, <=24 chars","Summary":"<=2 sentences"}' \
|
||||
--hints '["What is it?","Who benefits?","What problem?"]' \
|
||||
--prompt "Describe your initial feature request"
|
||||
```
|
||||
|
||||
**Installer integration**:
|
||||
```bash
|
||||
python setup_cascadingdev.py \
|
||||
--target /path/to/project \
|
||||
--provider claude \
|
||||
--claude-cmd /usr/local/bin/claude
|
||||
```
|
||||
|
||||
### Benefits of META + Ramble System
|
||||
|
||||
1. **No Hardcoding**: Field lists and validation rules live in templates
|
||||
2. **Dynamic Forms**: GUI adapts to template changes automatically
|
||||
3. **Consistent UX**: Same Ramble workflow for all template types
|
||||
4. **Extensible**: Add new providers without changing core logic
|
||||
5. **Offline Capable**: Mock provider works without network
|
||||
6. **AI-Assisted**: Users get help articulating complex requirements
|
||||
7. **Reversible**: All input is stored in git, easily editable later
|
||||
|
||||
### Limitations & Future Work
|
||||
|
||||
**Current Limitations**:
|
||||
- No automatic field validation (relies on AI compliance)
|
||||
- PlantUML rendering requires external CLI tool
|
||||
- Claude provider requires separate CLI installation
|
||||
- No streaming/incremental updates during generation
|
||||
|
||||
**Potential Enhancements** (not yet planned):
|
||||
- Native API providers (no CLI subprocess)
|
||||
- Real-time field validation
|
||||
- Multi-turn conversation support
|
||||
- Provider comparison mode (generate with multiple providers)
|
||||
- Template validator that checks META integrity
|
||||
|
||||
---
|
||||
|
||||
### Build & Release Process (repeatable)
|
||||
|
||||
Goal: deterministic “unzip + run” artifact for each version.
|
||||
|
||||
**Always rebuild after edits**
|
||||
```bash
|
||||
# Rebuild bundle every time you change assets/ or installer logic
|
||||
python tools/build_installer.py
|
||||
|
||||
# Run ONLY the bundled copy
|
||||
python install/cascadingdev-*/setup_cascadingdev.py --target /path/to/new-project
|
||||
```
|
||||
|
||||
**6.1 Versioning**
|
||||
- Update `VERSION` (semver): `MAJOR.MINOR.PATCH`
|
||||
- Tag releases in git to match `VERSION`
|
||||
|
|
@ -315,7 +837,6 @@ python setup_cascadingdev.py --target /path/to/users-project
|
|||
6. Makes initial commit
|
||||
|
||||
If GUI fails, use a virtualenv and \`pip install PySide6\`, or run with \`--no-ramble\`.
|
||||
```
|
||||
|
||||
This ensures every distributed bundle includes explicit usage instructions.
|
||||
|
||||
|
|
@ -822,7 +1343,7 @@ rules:
|
|||
feature_request:
|
||||
outputs:
|
||||
feature_discussion:
|
||||
path: "{dir}/discussions/feature.discussion.md"
|
||||
path: "{dir}/discussions/feature.feature.discussion.md"
|
||||
output_type: "feature_discussion_writer"
|
||||
instruction: |
|
||||
If missing: create with standard header (stage: feature, status: OPEN),
|
||||
|
|
@ -842,7 +1363,7 @@ rules:
|
|||
outputs:
|
||||
# 1) Append the new AI comment to the discussion (append-only)
|
||||
self_append:
|
||||
path: "{dir}/discussions/feature.discussion.md"
|
||||
path: "{dir}/discussions/feature.feature.discussion.md"
|
||||
output_type: "feature_discussion_writer"
|
||||
instruction: |
|
||||
Append concise comment signed with AI name, ending with a single vote line.
|
||||
|
|
@ -862,7 +1383,7 @@ rules:
|
|||
|
||||
# 3) Promotion artifacts when READY_FOR_DESIGN
|
||||
design_discussion:
|
||||
path: "{dir}/discussions/design.discussion.md"
|
||||
path: "{dir}/discussions/design.feature.discussion.md"
|
||||
output_type: "design_discussion_writer"
|
||||
instruction: |
|
||||
Create ONLY if feature discussion status is READY_FOR_DESIGN.
|
||||
|
|
@ -902,7 +1423,7 @@ rules:
|
|||
Update only the marker-bounded sections from the discussion content.
|
||||
|
||||
impl_discussion:
|
||||
path: "{dir}/discussions/implementation.discussion.md"
|
||||
path: "{dir}/discussions/implementation.feature.discussion.md"
|
||||
output_type: "impl_discussion_writer"
|
||||
instruction: |
|
||||
Create ONLY if design discussion status is READY_FOR_IMPLEMENTATION.
|
||||
|
|
@ -946,7 +1467,7 @@ rules:
|
|||
Include unchecked items from ../implementation/tasks.md in ACTION_ITEMS.
|
||||
|
||||
test_discussion:
|
||||
path: "{dir}/discussions/testing.discussion.md"
|
||||
path: "{dir}/discussions/testing.feature.discussion.md"
|
||||
output_type: "test_discussion_writer"
|
||||
instruction: |
|
||||
Create ONLY if implementation status is READY_FOR_TESTING.
|
||||
|
|
@ -996,7 +1517,7 @@ rules:
|
|||
Initialize bug discussion and fix plan in the same folder.
|
||||
|
||||
review_discussion:
|
||||
path: "{dir}/discussions/review.discussion.md"
|
||||
path: "{dir}/discussions/review.feature.discussion.md"
|
||||
output_type: "review_discussion_writer"
|
||||
instruction: |
|
||||
Create ONLY if all test checklist items pass.
|
||||
|
|
@ -1113,7 +1634,7 @@ resolve_template() {
|
|||
ext="${basename##*.}"
|
||||
# nearest FR_* ancestor as feature_id
|
||||
feature_id="$(echo "$rel_path" | sed -n 's|.*Docs/features/\(FR_[^/]*\).*|\1|p')"
|
||||
# infer stage from <stage>.discussion.md when applicable
|
||||
# infer stage from <stage>.feature.discussion.md when applicable
|
||||
stage="$(echo "$basename" | sed -n 's/^\([A-Za-z0-9_-]\+\)\.discussion\.md$/\1/p')"
|
||||
echo "$tmpl" \
|
||||
| sed -e "s|{date}|$today|g" \
|
||||
|
|
@ -1260,7 +1781,7 @@ Rule Definition (in Docs/features/.ai-rules.yml):
|
|||
discussion_moderator_nudge:
|
||||
outputs:
|
||||
self_append:
|
||||
path: "{dir}/discussions/{stage}.discussion.md"
|
||||
path: "{dir}/discussions/{stage}.feature.discussion.md"
|
||||
output_type: "discussion_moderator_writer"
|
||||
instruction: |
|
||||
Act as AI_Moderator. Analyze the entire discussion and:
|
||||
|
|
@ -1397,7 +1918,7 @@ Bypass & Minimal Patch:
|
|||
|
||||
```bash
|
||||
.git/ai-rules-debug/
|
||||
├─ 20251021-143022-12345-feature.discussion.md/
|
||||
├─ 20251021-143022-12345-feature.feature.discussion.md/
|
||||
│ ├─ raw.out # Raw model output
|
||||
│ ├─ clean.diff # Extracted patch
|
||||
│ ├─ sanitized.diff # After sanitization
|
||||
|
|
@ -1947,7 +2468,7 @@ Docs/features/FR_.../
|
|||
type: discussion-summary
|
||||
stage: feature # feature|design|implementation|testing|review
|
||||
status: ACTIVE # ACTIVE|SNAPSHOT|ARCHIVED
|
||||
source_discussion: feature.discussion.md
|
||||
source_discussion: feature.feature.discussion.md
|
||||
feature_id: FR_YYYY-MM-DD_<slug>
|
||||
updated: YYYY-MM-DDTHH:MM:SSZ
|
||||
policy:
|
||||
|
|
@ -2193,13 +2714,17 @@ Process Overhead:
|
|||
- Flexibility: Bypass options for trivial changes
|
||||
|
||||
## Initial Setup & Bootstrapping
|
||||
To streamline project onboarding and ensure every repository begins with a structured, traceable starting point, this system includes a one-time setup script that initializes the folder structure and guides the maintainer through creating the first feature request using the interactive dialog.
|
||||
To streamline project onboarding and ensure every repository begins with a structured, traceable starting point, this system includes:
|
||||
- a one-time setup script (`setup_cascadingdev.py`) that initializes the folder structure and installs the hook,
|
||||
- a `create_feature.py` tool for creating feature requests (with or without Ramble),
|
||||
- and a concise `USER_GUIDE.md` in the user project for daily guidance.
|
||||
|
||||
### Steps Performed:
|
||||
- Create the canonical folder structure under Docs/features/FR_<date>_initial-feature-request/, including the request.md template.
|
||||
- Run the interactive dialog utility to guide the user (or team) through describing the project’s intent, motivation, and constraints in natural language.
|
||||
- Initialize Git hooks, orchestration scripts, and default configuration files.
|
||||
- Automatically generate the first Feature Request document from that conversation.
|
||||
- Create the canonical folder structure under `Docs/` and seed the initial FR folder.
|
||||
- Install the pre-commit hook and default configuration files.
|
||||
- Copy `create_feature.py` and (optionally) `ramble.py` into the user project root.
|
||||
- Optionally run Ramble to help collect the first feature; otherwise prompt via CLI.
|
||||
- Generate the first Feature Request folder and the initial discussion + summary.
|
||||
|
||||
Example Implementation
|
||||
```python
|
||||
|
|
@ -2232,7 +2757,7 @@ def main():
|
|||
# Run Ramble dialog to fill in details interactively
|
||||
print("Launching Ramble interactive prompt...")
|
||||
run_ramble()
|
||||
print("Setup complete — initial feature request created.")
|
||||
print("Setup complete — run create_feature.py to add more features.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -2247,6 +2772,10 @@ Template Location as Version:
|
|||
- Breaking changes require new feature request and migration plan
|
||||
- Existing features use templates current at their creation
|
||||
|
||||
**User Guide**
|
||||
- The authoritative `USER_GUIDE.md` lives in CascadingDev’s source (`assets/templates/USER_GUIDE.md`) and is copied
|
||||
into user projects (root) at install time. Update the source and rebuild the bundle to propagate changes.
|
||||
|
||||
Migration Guidance:
|
||||
- Document template changes in release notes
|
||||
- Provide automated migration scripts for simple changes
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# CascadingDev Installer
|
||||
|
||||
## Requirements
|
||||
- Python 3.10+ and git
|
||||
- (Optional) PySide6 for GUI (`pip install PySide6`)
|
||||
|
||||
## Quick start
|
||||
```bash
|
||||
|
||||
python setup_cascadingdev.py --target /path/to/new-project
|
||||
```
|
||||
|
||||
### Skip GUI
|
||||
```bash
|
||||
python setup_cascadingdev.py --target /path/to/new-project --no-ramble
|
||||
```
|
||||
|
||||
> After installation, open `USER_GUIDE.md` in your new project for daily usage.
|
||||
|
||||
## Rebuild & Run (for maintainers)
|
||||
Rebuild the bundle every time you change assets/ or the installer:
|
||||
```bash
|
||||
python tools/build_installer.py
|
||||
```
|
||||
Then run only the bundled copy:
|
||||
```bash
|
||||
python install/cascadingdev-*/setup_cascadingdev.py --target /path/to/new-project
|
||||
```
|
||||
|
|
@ -1,9 +1,21 @@
|
|||
# pyproject.toml
|
||||
[build-system]
|
||||
requires = ["setuptools>=68", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "cascadingdev"
|
||||
version = "0.1.0"
|
||||
# Tell PEP 621 that version is provided dynamically
|
||||
dynamic = ["version"]
|
||||
description = "CascadingDev: scaffold rule-driven multi-agent project repos"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
[project.scripts]
|
||||
cdev = "cascadingdev.cli:main"
|
||||
|
||||
[tool.setuptools]
|
||||
package-dir = {"" = "src"}
|
||||
packages = ["cascadingdev"]
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = { file = "VERSION" }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
# src/cascadingdev/__init__.py
|
||||
from .utils import read_version
|
||||
__all__ = ["cli"]
|
||||
__version__ = read_version()
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
# 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)")
|
||||
p_bs = sub.add_parser("bundle-smoke", help="Unpack the zip and run installer into a temp dir")
|
||||
p_bs.add_argument("--keep", action="store_true")
|
||||
p_bs.add_argument("--ramble", action="store_true")
|
||||
p_bs.add_argument("--bundle", help="Path to installer zip")
|
||||
p_bs.add_argument("--target", help="Write demo repo to this path")
|
||||
|
||||
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" / "process" / "policies.yml",
|
||||
ROOT / "assets" / "templates" / "rules" / "root.ai-rules.yml",
|
||||
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
|
||||
|
||||
if args.cmd == "bundle-smoke":
|
||||
cmd = [sys.executable, str(ROOT / "tools" / "bundle_smoke.py")]
|
||||
if args.keep: cmd.append("--keep")
|
||||
if args.ramble: cmd.append("--ramble")
|
||||
if args.bundle: cmd += ["--bundle", args.bundle]
|
||||
if args.target: cmd += ["--target", args.target]
|
||||
return run(cmd)
|
||||
|
||||
ap.print_help()
|
||||
return 0
|
||||
|
|
@ -2,85 +2,194 @@
|
|||
"""
|
||||
setup_project.py — Installer-mode bootstrap for Cascading Development
|
||||
|
||||
Run this from your installation folder (NOT inside the destination repo):
|
||||
Run this from the **installer bundle folder** (e.g., install/cascadingdev-<version>/), 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)
|
||||
- Copies essential files from installer → target (ramble.py, templates, 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
|
||||
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 json, re
|
||||
import argparse
|
||||
import subprocess
|
||||
import datetime
|
||||
import sys
|
||||
import subprocess
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
INSTALL_ROOT = Path(__file__).resolve().parent.parent # installer root (contains this scripts/ dir)
|
||||
# Bundle root (must contain assets/, ramble.py, VERSION)
|
||||
INSTALL_ROOT = Path(__file__).resolve().parent
|
||||
if not (INSTALL_ROOT / "assets").exists():
|
||||
print("[-] This script must be run from the installer bundle directory (assets/ missing).")
|
||||
print(
|
||||
" Rebuild the bundle (e.g., `python tools/build_installer.py`) and run the copy in install/cascadingdev-*/.")
|
||||
sys.exit(2)
|
||||
|
||||
# ---------- 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)
|
||||
# ---------- Helper Functions ----------
|
||||
|
||||
def write_if_missing(path: Path, content: str):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
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):
|
||||
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()
|
||||
|
||||
# --- Tiny template helpers ----------------------------------------------------
|
||||
# Self-contained; no external dependencies
|
||||
_META_RE = re.compile(r"<!--META\s*(\{.*?\})\s*-->", re.S)
|
||||
|
||||
def load_template_with_meta(path: Path) -> tuple[dict, str]:
|
||||
"""
|
||||
Returns (meta: dict, body_without_meta: str). If no META, ({} , full text).
|
||||
META must be a single JSON object inside <!--META ... -->.
|
||||
"""
|
||||
if not path.exists():
|
||||
return {}, ""
|
||||
text = path.read_text(encoding="utf-8")
|
||||
m = _META_RE.search(text)
|
||||
if not m:
|
||||
return {}, text
|
||||
meta_json = m.group(1)
|
||||
try:
|
||||
meta = json.loads(meta_json)
|
||||
except Exception:
|
||||
meta = {}
|
||||
body = _META_RE.sub("", text, count=1).lstrip()
|
||||
return meta, body
|
||||
|
||||
def render_placeholders(body: str, values: dict) -> str:
|
||||
"""
|
||||
Simple {Token} replacement. Leaves unknown tokens as-is.
|
||||
"""
|
||||
# two-pass: {{Token}} then {Token}
|
||||
out = body
|
||||
for k, v in values.items():
|
||||
out = out.replace("{{" + k + "}}", str(v))
|
||||
try:
|
||||
out = out.format_map({k: v for k, v in values.items()})
|
||||
except Exception:
|
||||
pass
|
||||
return out
|
||||
|
||||
def meta_ramble_config(meta: dict) -> tuple[list[str], dict, dict, list[str]]:
|
||||
"""
|
||||
From template META, extract:
|
||||
- fields: list of field names in order
|
||||
- defaults: {field: default_value}
|
||||
- criteria: {field: rule/description} (optional)
|
||||
- hints: [str, ...] (optional)
|
||||
"""
|
||||
fields: list[str] = []
|
||||
defaults: dict = {}
|
||||
for spec in meta.get("ramble_fields", []):
|
||||
name = spec.get("name")
|
||||
if name:
|
||||
fields.append(name)
|
||||
if "default" in spec:
|
||||
defaults[name] = spec["default"]
|
||||
criteria = meta.get("criteria", {}) or {}
|
||||
hints = meta.get("hints", []) or []
|
||||
return fields, defaults, criteria, hints
|
||||
|
||||
def ensure_git_repo(target: Path):
|
||||
"""Initialize a git repository if one doesn't exist at the target path."""
|
||||
if not (target / ".git").exists():
|
||||
sh(["git", "init", "-b", "main"], cwd=str(target))
|
||||
# Initialize git repo with main branch
|
||||
run(["git", "init", "-b", "main"], cwd=target)
|
||||
# Seed .gitignore from template if present; otherwise fallback
|
||||
tmpl_gitignore = INSTALL_ROOT / "assets" / "templates" / "root_gitignore"
|
||||
if tmpl_gitignore.exists():
|
||||
copy_if_missing(tmpl_gitignore, target / ".gitignore")
|
||||
else:
|
||||
write_if_missing(target / ".gitignore", "\n".join([
|
||||
".env", ".env.*", "secrets/", ".git/ai-rules-*", "__pycache__/",
|
||||
"*.pyc", ".pytest_cache/", ".DS_Store",
|
||||
"__pycache__/", "*.py[cod]", "*.egg-info/", ".pytest_cache/",
|
||||
".mypy_cache/", ".coverage", "htmlcov/", "node_modules/",
|
||||
"dist/", "build/", ".env", ".env.*", "secrets/", ".DS_Store",
|
||||
".git/ai-rules-*",
|
||||
]) + "\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"
|
||||
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.")
|
||||
say("[-] pre-commit hook source missing at assets/hooks/pre-commit in the installer bundle.")
|
||||
return
|
||||
|
||||
# Copy hook content and make it executable
|
||||
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", claude_cmd: str = "claude"):
|
||||
"""
|
||||
Launch Ramble GUI to collect initial feature request details.
|
||||
Falls back to terminal prompts if GUI fails or returns invalid JSON.
|
||||
"""
|
||||
# Find FR template + read META (for field names)
|
||||
fr_tmpl = INSTALL_ROOT / "assets" / "templates" / "feature_request.md"
|
||||
meta, _ = load_template_with_meta(fr_tmpl)
|
||||
field_names, _defaults, criteria, hints = meta_ramble_config(meta)
|
||||
|
||||
# Fallback to your previous default fields if template lacks META
|
||||
if not field_names:
|
||||
field_names = ["Summary", "Title", "Intent", "ProblemItSolves", "BriefOverview"]
|
||||
|
||||
ramble = target / "ramble.py"
|
||||
if not ramble.exists():
|
||||
say("[-] ramble.py not found in target; skipping interactive FR capture.")
|
||||
return None
|
||||
|
||||
# Build Ramble arguments dynamically from the template-defined fields
|
||||
args = [
|
||||
sys.executable, str(ramble),
|
||||
"--provider", provider,
|
||||
"--claude-cmd", claude_cmd,
|
||||
"--prompt", "Describe your initial feature request for this repository",
|
||||
"--fields", "Summary", "Title", "Intent", "ProblemItSolves", "BriefOverview",
|
||||
"--criteria", '{"Summary":"<= 2 sentences","Title":"camelCase, <= 24 chars"}'
|
||||
"--fields", *field_names,
|
||||
]
|
||||
|
||||
if criteria:
|
||||
args += ["--criteria", json.dumps(criteria)]
|
||||
if hints:
|
||||
args += ["--hints", json.dumps(hints)]
|
||||
|
||||
say("[•] Launching Ramble (close the dialog with Submit to return JSON)…")
|
||||
proc = subprocess.run(args, text=True, capture_output=True, cwd=str(target))
|
||||
|
||||
# Show any stderr output from Ramble
|
||||
if proc.stderr and proc.stderr.strip():
|
||||
say("[Ramble stderr]")
|
||||
say(proc.stderr.strip())
|
||||
|
||||
# Try to parse JSON output from Ramble
|
||||
out = (proc.stdout or "").strip()
|
||||
if out:
|
||||
try:
|
||||
|
|
@ -88,8 +197,9 @@ def run_ramble_and_collect(target: Path, provider: str = "mock", claude_cmd: str
|
|||
except Exception as e:
|
||||
say(f"[-] JSON parse failed: {e}")
|
||||
|
||||
# Terminal fallback so setup can proceed without GUI deps
|
||||
# Terminal fallback - collect input manually if GUI fails
|
||||
say("[!] Falling back to terminal prompts.")
|
||||
|
||||
def ask(label, default=""):
|
||||
try:
|
||||
v = input(f"{label}: ").strip()
|
||||
|
|
@ -97,6 +207,7 @@ def run_ramble_and_collect(target: Path, provider: str = "mock", claude_cmd: str
|
|||
except EOFError:
|
||||
return default
|
||||
|
||||
# Collect required fields via terminal input
|
||||
fields = {
|
||||
"Title": ask("Title (camelCase, <=24 chars)", "initialProjectDesign"),
|
||||
"Intent": ask("Intent", "—"),
|
||||
|
|
@ -106,118 +217,99 @@ def run_ramble_and_collect(target: Path, provider: str = "mock", claude_cmd: str
|
|||
}
|
||||
return {"fields": fields, "summary": fields.get("Summary", "")}
|
||||
|
||||
|
||||
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
|
||||
""")
|
||||
"""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)
|
||||
|
||||
tmpl_dir = target / "process" / "templates"
|
||||
write_if_missing(tmpl_dir / "feature_request.md",
|
||||
"# Feature Request: <title>\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")
|
||||
# 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"
|
||||
|
||||
write_if_missing(target / ".ai-rules.yml",
|
||||
"""version: 1
|
||||
file_associations:
|
||||
"*.md": "md-file"
|
||||
# Copy policies
|
||||
if t_process.exists():
|
||||
copy_if_missing(t_process, process_dir / "policies.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 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")
|
||||
|
||||
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.
|
||||
""")
|
||||
# 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):
|
||||
today = datetime.date.today().isoformat()
|
||||
fr_dir = target / "Docs" / "features" / f"FR_{today}_initial-feature-request"
|
||||
feature_id = f"FR_{today}_initial-feature-request"
|
||||
fr_dir = target / "Docs" / "features" / feature_id
|
||||
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}
|
||||
# Gather values from Ramble result (if any)
|
||||
fields = (req_fields or {}).get("fields", {}) if req_fields else {}
|
||||
# Load FR template + META
|
||||
fr_tmpl = INSTALL_ROOT / "assets" / "templates" / "feature_request.md"
|
||||
fr_meta, fr_body = load_template_with_meta(fr_tmpl)
|
||||
field_names, defaults, _criteria, _hints = meta_ramble_config(fr_meta)
|
||||
|
||||
# Build values map with defaults → ramble fields → system tokens
|
||||
values = {}
|
||||
values.update(defaults) # template defaults
|
||||
values.update(fields) # user-entered
|
||||
values.update({ # system tokens
|
||||
"FeatureId": feature_id,
|
||||
"CreatedDate": today,
|
||||
})
|
||||
|
||||
# If no template body, fall back to your old default
|
||||
if not fr_body.strip():
|
||||
title = values.get("Title", "initialProjectDesign")
|
||||
intent = values.get("Intent", "—")
|
||||
problem = values.get("ProblemItSolves", "—")
|
||||
brief = values.get("BriefOverview", "—")
|
||||
summary = values.get("Summary", "")
|
||||
fr_body = f"""# Feature Request: {title}
|
||||
|
||||
**Intent**: {intent}
|
||||
**Motivation / Problem**: {problem}
|
||||
**Brief Overview**: {brief}
|
||||
|
||||
**Summary**: {summary}
|
||||
|
||||
**Meta**: Created: {today}
|
||||
"""
|
||||
|
||||
(fr_dir / "request.md").write_text(render_placeholders(fr_body, values), encoding="utf-8")
|
||||
|
||||
# --- feature.discussion.md ---
|
||||
disc_tmpl = INSTALL_ROOT / "assets" / "templates" / "feature.discussion.md"
|
||||
d_meta, d_body = load_template_with_meta(disc_tmpl)
|
||||
# Always include the front-matter for rules, then template body (or fallback)
|
||||
fm = f"""---\ntype: discussion\nstage: feature\nstatus: OPEN\nfeature_id: {feature_id}\ncreated: {today}\n---\n"""
|
||||
if not d_body.strip():
|
||||
d_body = (
|
||||
"## Summary\n"
|
||||
f"Initial discussion for {feature_id}. Append your comments below.\n\n"
|
||||
"## Participation\n"
|
||||
"- Maintainer: Kickoff. VOTE: READY\n"
|
||||
)
|
||||
(disc_dir / "feature.discussion.md").write_text(fm + render_placeholders(d_body, values), encoding="utf-8")
|
||||
|
||||
# --- feature.discussion.sum.md ---
|
||||
sum_tmpl = INSTALL_ROOT / "assets" / "templates" / "feature.discussion.sum.md"
|
||||
s_meta, s_body = load_template_with_meta(sum_tmpl)
|
||||
if s_body.strip():
|
||||
# use template
|
||||
(disc_dir / "feature.discussion.sum.md").write_text(render_placeholders(s_body, values), encoding="utf-8")
|
||||
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")
|
||||
|
||||
# your existing static content
|
||||
(disc_dir / "feature.discussion.sum.md").write_text(
|
||||
"""# Summary — Feature
|
||||
|
||||
|
|
@ -258,33 +350,49 @@ READY: 1 • CHANGES: 0 • REJECT: 0
|
|||
<!-- 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 essential files from the installer to the target repository."""
|
||||
# Runtime helpers into project root
|
||||
copy_if_exists(INSTALL_ROOT / "ramble.py", target / "ramble.py")
|
||||
copy_if_exists(INSTALL_ROOT / "create_feature.py", target / "create_feature.py")
|
||||
|
||||
# User guide into project root
|
||||
copy_if_exists(INSTALL_ROOT / "assets" / "templates" / "USER_GUIDE.md",
|
||||
target / "USER_GUIDE.md")
|
||||
|
||||
# Copy shipped templates (preferred source of truth)
|
||||
tmpl_src = INSTALL_ROOT / "assets" / "templates"
|
||||
if tmpl_src.exists():
|
||||
shutil.copytree(tmpl_src, target / "process" / "templates", dirs_exist_ok=True)
|
||||
|
||||
# Copy the hook (you already install it to .git/hooks via install_precommit_hook)
|
||||
# If you ever want the raw hook inside the user's repo too:
|
||||
# copy_if_exists(INSTALL_ROOT / "assets" / "hooks" / "pre-commit", target / "scripts" / "hooks" / "pre-commit")
|
||||
# Place USER_GUIDE.md under process/ (clear separation from source templates)
|
||||
ug_src = tmpl_src / "USER_GUIDE.md"
|
||||
if ug_src.exists():
|
||||
(target / "process").mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(ug_src, target / "process" / "USER_GUIDE.md")
|
||||
|
||||
# Hook is installed into .git/hooks by install_precommit_hook()
|
||||
|
||||
# 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):
|
||||
"""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
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for the Cascading Development setup script."""
|
||||
# Parse command line arguments
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--target", help="Destination path to create/use the repo")
|
||||
ap.add_argument("--provider", choices=["mock", "claude"], default="mock", help="Ramble provider (default: mock)")
|
||||
|
|
@ -292,6 +400,7 @@ def main():
|
|||
ap.add_argument("--claude-cmd", default="claude")
|
||||
args = ap.parse_args()
|
||||
|
||||
# Get target directory from args or prompt user
|
||||
target_str = args.target
|
||||
if not target_str:
|
||||
target_str = input("Destination repo path (will be created if missing): ").strip()
|
||||
|
|
@ -299,15 +408,16 @@ def main():
|
|||
say("No target specified. Aborting.")
|
||||
sys.exit(2)
|
||||
|
||||
# Resolve and create target directory
|
||||
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
|
||||
# Step 1: Copy assets from installer into target
|
||||
copy_install_assets_to_target(target)
|
||||
|
||||
# Ensure folder layout
|
||||
# Step 2: Create standard folder structure
|
||||
for p in [
|
||||
target / "Docs" / "features",
|
||||
target / "Docs" / "discussions" / "reviews",
|
||||
|
|
@ -319,26 +429,28 @@ def main():
|
|||
]:
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create rules/templates and basic process docs
|
||||
# Step 3: Create rules/templates and basic process docs
|
||||
seed_process_and_rules(target)
|
||||
|
||||
# Initialize git & install pre-commit
|
||||
# Step 4: Initialize git & install pre-commit
|
||||
ensure_git_repo(target)
|
||||
install_precommit_hook(target)
|
||||
|
||||
# Launch Ramble (if available)
|
||||
# Step 5: Launch Ramble (if available and not disabled)
|
||||
req = None
|
||||
if not args.no_ramble:
|
||||
req = run_ramble_and_collect(target, provider=args.provider, claude_cmd=args.claude_cmd)
|
||||
|
||||
# Seed first feature based on Ramble output
|
||||
# Step 6: Seed first feature based on Ramble output
|
||||
seed_initial_feature(target, req)
|
||||
|
||||
# First commit
|
||||
# Step 7: Perform initial commit
|
||||
first_commit(target)
|
||||
|
||||
# Completion message
|
||||
say("[✓] Setup complete.")
|
||||
say(f"Next steps:\n cd {target}\n git status")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -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,46 @@ 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",
|
||||
"feature.discussion.md",
|
||||
"feature.discussion.sum.md",
|
||||
"design_doc.md",
|
||||
"USER_GUIDE.md",
|
||||
"root_gitignore",
|
||||
]:
|
||||
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,74 @@
|
|||
#!/usr/bin/env python3
|
||||
import tempfile, shutil, subprocess, sys, argparse
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--bundle", help="Path to a specific installer zip (defaults to install/cascadingdev-<VERSION>.zip)")
|
||||
ap.add_argument("--keep", action="store_true", help="Keep temporary directory for inspection")
|
||||
ap.add_argument("--target", help="Write the demo repo to this path instead of a temp dir")
|
||||
ap.add_argument("--ramble", action="store_true", help="Run installer without --no-ramble")
|
||||
args = ap.parse_args()
|
||||
|
||||
ver = (ROOT / "VERSION").read_text().strip()
|
||||
zip_path = Path(args.bundle) if args.bundle else (ROOT / "install" / f"cascadingdev-{ver}.zip")
|
||||
if not zip_path.exists():
|
||||
print(f"Zip missing: {zip_path}. Run `cdev pack` first or pass --bundle."); sys.exit(2)
|
||||
|
||||
def run_once(extract_dir: Path, target_dir: Path) -> int:
|
||||
shutil.unpack_archive(str(zip_path), str(extract_dir), "zip")
|
||||
setup = next(extract_dir.rglob("setup_cascadingdev.py"))
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
print(f"[•] Running installer from: {setup}")
|
||||
cmd = [sys.executable, str(setup), "--target", str(target_dir)]
|
||||
if not args.ramble:
|
||||
cmd.append("--no-ramble")
|
||||
rc = subprocess.call(cmd)
|
||||
if rc != 0:
|
||||
print(f"Installer exited with {rc}"); return rc
|
||||
# quick asserts
|
||||
required = [
|
||||
target_dir / "process" / "policies.yml",
|
||||
target_dir / "Docs" / "features" / ".ai-rules.yml",
|
||||
target_dir / ".ai-rules.yml",
|
||||
target_dir / "USER_GUIDE.md",
|
||||
target_dir / ".git" / "hooks" / "pre-commit",
|
||||
]
|
||||
missing = [str(p) for p in required if not p.exists()]
|
||||
if missing:
|
||||
print("Missing after install:\n " + "\n ".join(missing)); return 3
|
||||
print(f"[✓] Bundle smoke OK. Demo repo: {target_dir}")
|
||||
return 0
|
||||
|
||||
if args.target:
|
||||
# Use a fixed location (never auto-delete). Clean if exists.
|
||||
target_dir = Path(args.target).expanduser().resolve()
|
||||
if target_dir.exists():
|
||||
shutil.rmtree(target_dir)
|
||||
extract_dir = target_dir.parent / (target_dir.name + "-bundle")
|
||||
if extract_dir.exists():
|
||||
shutil.rmtree(extract_dir)
|
||||
extract_dir.mkdir(parents=True, exist_ok=True)
|
||||
rc = run_once(extract_dir, target_dir)
|
||||
print(f"[i] Extracted bundle kept at: {extract_dir}")
|
||||
return rc
|
||||
|
||||
# Temp-mode (default)
|
||||
with tempfile.TemporaryDirectory(prefix="cd-bundle-") as tmp:
|
||||
tmpdir = Path(tmp)
|
||||
extract_dir = tmpdir / "bundle"
|
||||
target_dir = tmpdir / "demo-repo"
|
||||
rc = run_once(extract_dir, target_dir)
|
||||
if args.keep:
|
||||
print(f"[i] Keeping temp dir: {tmpdir}")
|
||||
print(" You can inspect it now; press Enter to clean up...")
|
||||
try: input()
|
||||
except EOFError: pass
|
||||
return rc
|
||||
# auto-clean
|
||||
return rc
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(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" / "feature.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