Compare commits

..

No commits in common. "e37b1a97227c9393cdd84e10e833ecd4a2fab9d3" and "5506891a5237d8cbaabcac9bc69c8cfda2f45bc3" have entirely different histories.

26 changed files with 457 additions and 1989 deletions

1
.gitignore vendored
View File

@ -2,4 +2,3 @@
.idea/
__pycache__/
*.pyc
install/

101
CLAUDE.md
View File

@ -1,101 +0,0 @@
# 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

View File

@ -1,4 +1,4 @@
# CascadingDev - AIHuman Collaboration System
# Cascading Development - AIHuman Collaboration System
## Process & Architecture Design Document (v2.0)
- Feature ID: FR_2025-10-21_initial-feature-request
- Status: Design Approved (Ready for Implementation)
@ -29,8 +29,6 @@
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:
@ -59,198 +57,122 @@ Human → Git Commit → Pre-commit Hook → AI Generator → Markdown Artifact
Orchestrator ← Discussion Summaries ← AI Moderator
```
## 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.
## Repository Layout
### Canonical Structure (Per-Feature Folders)
```text
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
/ (repository root)
├─ .ai-rules.yml # Global defaults + file associations
├─ automation/ # Orchestrator & adapters
│ ├─ workflow.py # Python status/reporting (v1 non-blocking)
│ ├─ adapters/
│ │ ├─ 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.
---
### 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
│ │ ├─ 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
│ ├─ feature.discussion.md
│ ├─ feature.discussion.sum.md
│ ├─ discussion.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)
│ └─ 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/ # Auto-generated diagrams (future)
│ └─ diagrams/
│ └─ 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
├─ src/ # Application source code
└─ tests/ # System 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
├─ integration/
└─ bin/
```
**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)
---
The sections below describe the meta-infrastructure of CascadingDev itself — how it builds and distributes the installer that generates user projects.
## Installation & Distribution Architecture
### First-Run Flow (User's Project Initialization)
### Terminology (clarified)
- **CascadingDev** — this tooling project (the code in this repository).
- **Users project** — a new repository scaffolded by running CascadingDevs 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 users 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 (Users Project Initialization)
User runs:
```bash
@ -258,12 +180,10 @@ 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, and create_feature.py into the user project
- Creates standard folders (`Docs/`, `process/templates/`, etc.)
- Copies templates, `ramble.py`, `DESIGN.md`, and installs pre-commit hook
- Initializes git (main branch), writes `.gitignore`
- 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
- Launches Ramble (unless `--no-ramble`) to collect the first Feature Request
**Seeds:**
```
@ -276,7 +196,6 @@ Initial commit message: “bootstrap Cascading Development scaffolding”.
**Fallback:** If Ramble JSON isnt 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 features design doc (created later at the design stage) becomes the projects own design document.
### Pre-Commit Hook (v1 behavior)
- Fast regex secret scan on staged diffs
@ -285,452 +204,11 @@ Important: The CascadingDev DESIGN.md is not copied into user projects. The firs
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`
@ -837,6 +315,7 @@ 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.
@ -1343,7 +822,7 @@ rules:
feature_request:
outputs:
feature_discussion:
path: "{dir}/discussions/feature.feature.discussion.md"
path: "{dir}/discussions/feature.discussion.md"
output_type: "feature_discussion_writer"
instruction: |
If missing: create with standard header (stage: feature, status: OPEN),
@ -1363,7 +842,7 @@ rules:
outputs:
# 1) Append the new AI comment to the discussion (append-only)
self_append:
path: "{dir}/discussions/feature.feature.discussion.md"
path: "{dir}/discussions/feature.discussion.md"
output_type: "feature_discussion_writer"
instruction: |
Append concise comment signed with AI name, ending with a single vote line.
@ -1383,7 +862,7 @@ rules:
# 3) Promotion artifacts when READY_FOR_DESIGN
design_discussion:
path: "{dir}/discussions/design.feature.discussion.md"
path: "{dir}/discussions/design.discussion.md"
output_type: "design_discussion_writer"
instruction: |
Create ONLY if feature discussion status is READY_FOR_DESIGN.
@ -1423,7 +902,7 @@ rules:
Update only the marker-bounded sections from the discussion content.
impl_discussion:
path: "{dir}/discussions/implementation.feature.discussion.md"
path: "{dir}/discussions/implementation.discussion.md"
output_type: "impl_discussion_writer"
instruction: |
Create ONLY if design discussion status is READY_FOR_IMPLEMENTATION.
@ -1467,7 +946,7 @@ rules:
Include unchecked items from ../implementation/tasks.md in ACTION_ITEMS.
test_discussion:
path: "{dir}/discussions/testing.feature.discussion.md"
path: "{dir}/discussions/testing.discussion.md"
output_type: "test_discussion_writer"
instruction: |
Create ONLY if implementation status is READY_FOR_TESTING.
@ -1517,7 +996,7 @@ rules:
Initialize bug discussion and fix plan in the same folder.
review_discussion:
path: "{dir}/discussions/review.feature.discussion.md"
path: "{dir}/discussions/review.discussion.md"
output_type: "review_discussion_writer"
instruction: |
Create ONLY if all test checklist items pass.
@ -1634,7 +1113,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>.feature.discussion.md when applicable
# infer stage from <stage>.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" \
@ -1781,7 +1260,7 @@ Rule Definition (in Docs/features/.ai-rules.yml):
discussion_moderator_nudge:
outputs:
self_append:
path: "{dir}/discussions/{stage}.feature.discussion.md"
path: "{dir}/discussions/{stage}.discussion.md"
output_type: "discussion_moderator_writer"
instruction: |
Act as AI_Moderator. Analyze the entire discussion and:
@ -1918,7 +1397,7 @@ Bypass & Minimal Patch:
```bash
.git/ai-rules-debug/
├─ 20251021-143022-12345-feature.feature.discussion.md/
├─ 20251021-143022-12345-feature.discussion.md/
│ ├─ raw.out # Raw model output
│ ├─ clean.diff # Extracted patch
│ ├─ sanitized.diff # After sanitization
@ -2468,7 +1947,7 @@ Docs/features/FR_.../
type: discussion-summary
stage: feature # feature|design|implementation|testing|review
status: ACTIVE # ACTIVE|SNAPSHOT|ARCHIVED
source_discussion: feature.feature.discussion.md
source_discussion: feature.discussion.md
feature_id: FR_YYYY-MM-DD_<slug>
updated: YYYY-MM-DDTHH:MM:SSZ
policy:
@ -2714,17 +2193,13 @@ 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 (`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.
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.
### Steps Performed:
- 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.
- 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 projects 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.
Example Implementation
```python
@ -2757,7 +2232,7 @@ def main():
# Run Ramble dialog to fill in details interactively
print("Launching Ramble interactive prompt...")
run_ramble()
print("Setup complete — run create_feature.py to add more features.")
print("Setup complete — initial feature request created.")
if __name__ == "__main__":
main()
@ -2772,10 +2247,6 @@ 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 CascadingDevs 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

View File

@ -1,76 +0,0 @@
# CascadingDev (CDev) - Simplified
**CDev** — short for *Cascading Development* — is a **Git-native AIhuman 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

View File

@ -1 +0,0 @@
0.1.0

View File

@ -1,59 +1,18 @@
#!/usr/bin/env bash
#
# 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
# Safety settings: exit on errors, treat unset variables as errors, and catch pipeline failures
set -euo pipefail
if [[ -n "${CDEV_SKIP_HOOK:-}" ]]; then
exit 0
fi
# Navigate to git repository root so all file paths work correctly
# 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
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 matching common patterns
# Abort commit if staged changes contain potential secrets (api keys, tokens, etc.) 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."
@ -62,7 +21,7 @@ if echo "$DIFF" | grep -Eqi '(api[_-]?key|secret|access[_-]?token|private[_-]?ke
fi
# -------- ensure discussion summaries exist (companion files) ----------
if [[ -z "${CDEV_SKIP_SUMMARIES:-}" ]]; then
# Create and auto-stage a summary template file for any discussion file that doesn't already have one
ensure_summary() {
local disc="$1"
local dir; dir="$(dirname "$disc")"
@ -119,6 +78,11 @@ EOF
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

View File

@ -1,262 +0,0 @@
#!/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()

View File

@ -699,7 +699,6 @@ 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")
@ -734,23 +733,11 @@ 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 = hints,
hints=None,
provider=provider,
enable_stability=args.stability,
enable_pexels=args.pexels,

View File

@ -1,27 +0,0 @@
# Project Guide
## Core idea
- An **empty projects 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.

View File

@ -0,0 +1,25 @@
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

View File

@ -1,12 +0,0 @@
<!--META
{
"kind": "discussion",
"tokens": ["FeatureId", "CreatedDate"]
}
-->
## Summary
Initial discussion for {FeatureId}. Append your comments below.
## Participation
- Maintainer: Kickoff. VOTE: READY

View File

@ -1,44 +0,0 @@
<!--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 -->

View File

@ -1,35 +1,10 @@
# Feature Request: <title>
<!--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}
**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):

View File

@ -1,25 +0,0 @@
version: 1
voting:
values: [READY, CHANGES, REJECT]
allow_agent_votes: true
quorum:
discussion: { ready: all, reject: all }
design: { ready: all, reject: all }
implementation: { ready: 1_human, reject: all }
testing: { ready: all, reject: all }
review: { ready: 1_human, reject: all }
eligibility:
agents_allowed: true
require_human_for: [implementation, review]
etiquette:
name_prefix_agents: "AI_"
vote_line_regex: "^VOTE:\\s*(READY|CHANGES|REJECT)\\s*$"
response_timeout_hours: 24
timeouts:
discussion_stale_days: 3
nudge_interval_hours: 24
promotion_timeout_days: 14
security:
scanners:
enabled: true
tool: gitleaks

View File

@ -1,27 +0,0 @@
# 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-*

View File

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

View File

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

View File

@ -1,28 +0,0 @@
# 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
```

View File

@ -1,21 +1,9 @@
# pyproject.toml
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "cascadingdev"
# Tell PEP 621 that version is provided dynamically
dynamic = ["version"]
version = "0.1.0"
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" }

View File

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

View File

@ -1,82 +0,0 @@
# 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

View File

@ -2,194 +2,85 @@
"""
setup_project.py Installer-mode bootstrap for Cascading Development
Run this from the **installer bundle folder** (e.g., install/cascadingdev-<version>/), NOT inside the destination repo:
Run this from your installation folder (NOT inside the destination repo):
- Prompts (or use --target) for the destination repo path
- Copies essential files from installer target (ramble.py, templates, hooks)
- Copies essential files from installer target (DESIGN.md, ramble.py, hooks)
- Creates canonical structure, seeds rules/templates
- Initializes git and installs pre-commit hook
- Launches Ramble to capture the first feature request
Examples:
python setup_cascadingdev.py --target ~/dev/my-new-repo
python setup_cascadingdev.py --target /abs/path --no-ramble
python3 scripts/setup_project.py
python3 scripts/setup_project.py --target ~/dev/my-new-repo
python3 scripts/setup_project.py --target /abs/path --no-ramble
"""
import json, re
import argparse
import datetime
import sys
import subprocess
import json
import shutil
import argparse
import subprocess
import datetime
from pathlib import Path
# 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)
INSTALL_ROOT = Path(__file__).resolve().parent.parent # installer root (contains this scripts/ dir)
# ---------- helpers ----------
def sh(cmd, check=True, cwd=None):
return subprocess.run(cmd, check=check, text=True, capture_output=True, cwd=cwd)
# ---------- Helper Functions ----------
def say(msg): print(msg, flush=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)
def write_if_missing(path: Path, content: str):
path.parent.mkdir(parents=True, exist_ok=True)
if not path.exists():
path.write_text(content, encoding="utf-8")
def copy_if_exists(src: Path, dst: Path) -> None:
def copy_if_exists(src: Path, dst: Path):
if src.exists():
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
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(str(src), str(dst))
def ensure_git_repo(target: Path):
"""Initialize a git repository if one doesn't exist at the target path."""
if not (target / ".git").exists():
# Initialize git repo with main branch
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:
sh(["git", "init", "-b", "main"], cwd=str(target))
write_if_missing(target / ".gitignore", "\n".join([
"__pycache__/", "*.py[cod]", "*.egg-info/", ".pytest_cache/",
".mypy_cache/", ".coverage", "htmlcov/", "node_modules/",
"dist/", "build/", ".env", ".env.*", "secrets/", ".DS_Store",
".git/ai-rules-*",
".env", ".env.*", "secrets/", ".git/ai-rules-*", "__pycache__/",
"*.pyc", ".pytest_cache/", ".DS_Store",
]) + "\n")
def install_precommit_hook(target: Path):
"""Install the pre-commit hook from installer assets to target git hooks."""
hook_src = INSTALL_ROOT / "assets" / "hooks" / "pre-commit"
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 assets/hooks/pre-commit in the installer bundle.")
say("[-] pre-commit hook source missing at scripts/hooks/pre-commit in the installer.")
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", *field_names,
"--fields", "Summary", "Title", "Intent", "ProblemItSolves", "BriefOverview",
"--criteria", '{"Summary":"<= 2 sentences","Title":"camelCase, <= 24 chars"}'
]
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:
@ -197,9 +88,8 @@ 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 - collect input manually if GUI fails
# Terminal fallback so setup can proceed without GUI deps
say("[!] Falling back to terminal prompts.")
def ask(label, default=""):
try:
v = input(f"{label}: ").strip()
@ -207,7 +97,6 @@ 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", ""),
@ -217,99 +106,118 @@ 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):
"""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)
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
""")
# 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"
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")
# Copy policies
if t_process.exists():
copy_if_missing(t_process, process_dir / "policies.yml")
write_if_missing(target / ".ai-rules.yml",
"""version: 1
file_associations:
"*.md": "md-file"
# 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")
rules:
md-file:
description: "Normalize Markdown"
instruction: |
Keep markdown tidy (headings, lists, spacing). No content churn.
settings:
model: "local-mock"
temperature: 0.1
""")
# Discussion/feature rules (cascade/override within Docs/features)
if t_rules_features.exists():
copy_if_missing(t_rules_features, rules_dir / ".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.
""")
def seed_initial_feature(target: Path, req_fields: dict | None):
today = datetime.date.today().isoformat()
feature_id = f"FR_{today}_initial-feature-request"
fr_dir = target / "Docs" / "features" / feature_id
fr_dir = target / "Docs" / "features" / f"FR_{today}_initial-feature-request"
disc_dir = fr_dir / "discussions"
disc_dir.mkdir(parents=True, exist_ok=True)
# 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}
if req_fields:
title = (req_fields.get("fields", {}) or {}).get("Title", "").strip() or "initialProjectDesign"
intent = (req_fields.get("fields", {}) or {}).get("Intent", "").strip() or ""
problem = (req_fields.get("fields", {}) or {}).get("ProblemItSolves", "").strip() or ""
brief = (req_fields.get("fields", {}) or {}).get("BriefOverview", "").strip() or ""
summary = (req_fields.get("summary") or "").strip()
body = f"""# Feature Request: {title}
**Intent**: {intent}
**Motivation / Problem**: {problem}
**Brief Overview**: {brief}
**Summary**: {summary}
**Meta**: Created: {today}
"""
(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:
# your existing static content
body = (target / "process" / "templates" / "feature_request.md").read_text(encoding="utf-8")
(fr_dir / "request.md").write_text(body, encoding="utf-8")
(disc_dir / "feature.discussion.md").write_text(
f"""---
type: discussion
stage: feature
status: OPEN
feature_id: FR_{today}_initial-feature-request
created: {today}
---
## Summary
Initial discussion for the first feature request. Append your comments below.
## Participation
- Maintainer: Kickoff. VOTE: READY
""", encoding="utf-8")
(disc_dir / "feature.discussion.sum.md").write_text(
"""# Summary — Feature
@ -350,49 +258,33 @@ READY: 1 • CHANGES: 0 • REJECT: 0
<!-- SUMMARY:LINKS END -->
""".replace("{ts}", today), encoding="utf-8")
def copy_install_assets_to_target(target: Path):
"""Copy essential files from the installer to the target repository."""
# Runtime helpers into project root
# Copy DESIGN.md and ramble.py from installer if present
copy_if_exists(INSTALL_ROOT / "DESIGN.md", target / "DESIGN.md")
copy_if_exists(INSTALL_ROOT / "ramble.py", target / "ramble.py")
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)
# 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()
# 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")
# 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:
run(["git", "add", "-A"], cwd=target)
run(["git", "commit", "-m", "chore: bootstrap Cascading Development scaffolding"], cwd=target)
sh(["git", "add", "-A"], cwd=str(target))
sh(["git", "commit", "-m", "chore: bootstrap Cascading Development scaffolding"], cwd=str(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)")
@ -400,7 +292,6 @@ 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()
@ -408,16 +299,15 @@ 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}")
# Step 1: Copy assets from installer into target
# Copy assets from installer into target
copy_install_assets_to_target(target)
# Step 2: Create standard folder structure
# Ensure folder layout
for p in [
target / "Docs" / "features",
target / "Docs" / "discussions" / "reviews",
@ -429,28 +319,26 @@ def main():
]:
p.mkdir(parents=True, exist_ok=True)
# Step 3: Create rules/templates and basic process docs
# Create rules/templates and basic process docs
seed_process_and_rules(target)
# Step 4: Initialize git & install pre-commit
# Initialize git & install pre-commit
ensure_git_repo(target)
install_precommit_hook(target)
# Step 5: Launch Ramble (if available and not disabled)
# Launch Ramble (if available)
req = None
if not args.no_ramble:
req = run_ramble_and_collect(target, provider=args.provider, claude_cmd=args.claude_cmd)
# Step 6: Seed first feature based on Ramble output
# Seed first feature based on Ramble output
seed_initial_feature(target, req)
# Step 7: Perform initial commit
# First commit
first_commit(target)
# Completion message
say("[✓] Setup complete.")
say(f"Next steps:\n cd {target}\n git status")
if __name__ == "__main__":
main()

View File

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

View File

@ -8,46 +8,149 @@ 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)
# Create the directories
# copy essentials
(BUNDLE / "assets" / "hooks").mkdir(parents=True, exist_ok=True)
(BUNDLE / "assets" / "templates").mkdir(parents=True, exist_ok=True)
# Copy the git hook and any other runtime utilities.
shutil.copy2(ROOT / "DESIGN.md", BUNDLE / "DESIGN.md")
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")
# copy core templates
for t in [
"feature_request.md",
"feature.discussion.md",
"feature.discussion.sum.md",
"design_doc.md",
"USER_GUIDE.md",
"root_gitignore",
]:
for t in ["feature_request.md","discussion.md","design_doc.md"]:
shutil.copy2(ROOT / "assets" / "templates" / t, BUNDLE / "assets" / "templates" / t)
# copy (recursively) the contents of process/ and rules/ templates folders
def copy_tree(src, dst):
if dst.exists():
shutil.rmtree(dst)
shutil.copytree(src, dst)
copy_tree(ROOT / "assets" / "templates" / "process", BUNDLE / "assets" / "templates" / "process")
copy_tree(ROOT / "assets" / "templates" / "rules", BUNDLE / "assets" / "templates" / "rules")
# write installer entrypoint
shutil.copy2(ROOT / "src" / "cascadingdev" / "setup_project.py",
BUNDLE / "setup_cascadingdev.py")
(BUNDLE / "setup_cascadingdev.py").write_text(INSTALLER_PY, encoding="utf-8")
(BUNDLE / "INSTALL.md").write_text("Unzip, then run:\n\n python3 setup_cascadingdev.py\n", encoding="utf-8")
(BUNDLE / "VERSION").write_text(VER, encoding="utf-8")
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()

View File

@ -1,74 +0,0 @@
#!/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())

View File

@ -1,22 +0,0 @@
#!/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()