Compare commits
No commits in common. "e37b1a97227c9393cdd84e10e833ecd4a2fab9d3" and "5506891a5237d8cbaabcac9bc69c8cfda2f45bc3" have entirely different histories.
e37b1a9722
...
5506891a52
|
|
@ -2,4 +2,3 @@
|
||||||
.idea/
|
.idea/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
install/
|
|
||||||
|
|
|
||||||
101
CLAUDE.md
101
CLAUDE.md
|
|
@ -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
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# CascadingDev - AI–Human Collaboration System
|
# Cascading Development - AI–Human Collaboration System
|
||||||
## Process & Architecture Design Document (v2.0)
|
## Process & Architecture Design Document (v2.0)
|
||||||
- Feature ID: FR_2025-10-21_initial-feature-request
|
- Feature ID: FR_2025-10-21_initial-feature-request
|
||||||
- Status: Design Approved (Ready for Implementation)
|
- 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.
|
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.
|
> *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:
|
### Objective:
|
||||||
|
|
@ -59,198 +57,122 @@ Human → Git Commit → Pre-commit Hook → AI Generator → Markdown Artifact
|
||||||
Orchestrator ← Discussion Summaries ← AI Moderator
|
Orchestrator ← Discussion Summaries ← AI Moderator
|
||||||
```
|
```
|
||||||
|
|
||||||
## Repository Layouts
|
## Repository Layout
|
||||||
|
|
||||||
This section clarifies three different directory structures that are easy to confuse:
|
|
||||||
|
|
||||||
### Terminology
|
|
||||||
- **CascadingDev Repo** — The tooling project (this repository) that builds installers
|
|
||||||
- **Install Bundle** — The distributable artifact created by `tools/build_installer.py`
|
|
||||||
- **User Project** — A new repository scaffolded when a user runs the installer
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### A) CascadingDev Repository (Tooling Source)
|
|
||||||
|
|
||||||
This is the development repository where CascadingDev itself is maintained.
|
|
||||||
|
|
||||||
|
### Canonical Structure (Per-Feature Folders)
|
||||||
```text
|
```text
|
||||||
CascadingDev/ # This repository
|
/ (repository root)
|
||||||
├─ src/cascadingdev/ # Core Python modules
|
├─ .ai-rules.yml # Global defaults + file associations
|
||||||
│ ├─ cli.py # Developer CLI (cdev command)
|
├─ automation/ # Orchestrator & adapters
|
||||||
│ ├─ setup_project.py # Installer script (copied to bundle)
|
│ ├─ workflow.py # Python status/reporting (v1 non-blocking)
|
||||||
│ ├─ utils.py # Version management, utilities
|
|
||||||
│ ├─ feature_seed.py # Feature scaffolding logic
|
|
||||||
│ ├─ rules_seed.py # Rules seeding logic
|
|
||||||
│ ├─ fs_scaffold.py # Filesystem utilities
|
|
||||||
│ └─ ramble_integration.py # Ramble GUI integration
|
|
||||||
├─ assets/ # Single source of truth for shipped files
|
|
||||||
│ ├─ hooks/
|
|
||||||
│ │ └─ pre-commit # Git hook template (bash script)
|
|
||||||
│ ├─ templates/ # Templates copied to user projects
|
|
||||||
│ │ ├─ USER_GUIDE.md # Daily usage guide
|
|
||||||
│ │ ├─ feature_request.md # Feature request template
|
|
||||||
│ │ ├─ feature.discussion.md # Discussion template
|
|
||||||
│ │ ├─ feature.discussion.sum.md # Summary template
|
|
||||||
│ │ ├─ design_doc.md # Design document template
|
|
||||||
│ │ ├─ root_gitignore # Root .gitignore template
|
|
||||||
│ │ ├─ process/
|
|
||||||
│ │ │ └─ policies.yml # Machine-readable policies
|
|
||||||
│ │ └─ rules/
|
|
||||||
│ │ ├─ root.ai-rules.yml # Root cascading rules
|
|
||||||
│ │ └─ features.ai-rules.yml # Feature-level rules
|
|
||||||
│ └─ runtime/ # Scripts copied to bundle & user projects
|
|
||||||
│ ├─ ramble.py # GUI for feature creation (PySide6/PyQt5)
|
|
||||||
│ ├─ create_feature.py # CLI for feature creation
|
|
||||||
│ └─ .gitignore.seed # Gitignore seed patterns
|
|
||||||
├─ tools/ # Build and test automation
|
|
||||||
│ ├─ build_installer.py # Creates install bundle
|
|
||||||
│ ├─ smoke_test.py # Basic validation tests
|
|
||||||
│ └─ bundle_smoke.py # End-to-end installer testing
|
|
||||||
├─ install/ # Build output directory (git-ignored)
|
|
||||||
│ └─ cascadingdev-<version>/ # Generated installer bundle (see section B)
|
|
||||||
├─ docs/ # System documentation
|
|
||||||
│ ├─ DESIGN.md # This comprehensive design document
|
|
||||||
│ └─ INSTALL.md # Installation instructions
|
|
||||||
├─ tests/ # Test suite (planned, not yet implemented)
|
|
||||||
│ ├─ unit/
|
|
||||||
│ ├─ integration/
|
|
||||||
│ └─ bin/
|
|
||||||
├─ VERSION # Semantic version (e.g., 0.1.0)
|
|
||||||
├─ pyproject.toml # Python package configuration
|
|
||||||
├─ README.md # Public-facing project overview
|
|
||||||
└─ CLAUDE.md # AI assistant guidance
|
|
||||||
|
|
||||||
FUTURE (planned but not yet implemented):
|
|
||||||
├─ automation/ # 🚧 M1: Orchestration layer
|
|
||||||
│ ├─ workflow.py # Status reporting, vote parsing
|
|
||||||
│ ├─ adapters/
|
│ ├─ adapters/
|
||||||
│ │ ├─ claude_adapter.py # AI model integration
|
│ │ ├─ claude_adapter.py # Model interface (future)
|
||||||
│ │ └─ gitea_adapter.py # Gitea API integration
|
│ │ ├─ gitea_adapter.py # Gitea API integration (future)
|
||||||
│ └─ agents.yml # Agent role definitions
|
│ │ └─ agent_coordinator.py # Role routing & task allocation (future)
|
||||||
```
|
│ ├─ agents.yml # Role → stages mapping
|
||||||
|
│ └─ config.yml # Configuration (future)
|
||||||
**Purpose:** Development, testing, and building the installer. The `assets/` directory is the single source of truth for all files shipped to users.
|
├─ process/ # Process documentation & templates
|
||||||
|
│ ├─ design.md # This document
|
||||||
---
|
│ ├─ policies.md # Human-friendly policy documentation
|
||||||
|
│ ├─ policies.yml # Machine-readable policy configuration
|
||||||
### B) Install Bundle (Distribution Artifact)
|
│ └─ templates/
|
||||||
|
|
||||||
This is the self-contained, portable installer created by `tools/build_installer.py`.
|
|
||||||
|
|
||||||
```text
|
|
||||||
cascadingdev-<version>/ # Distributable bundle
|
|
||||||
├─ setup_cascadingdev.py # Installer entry point (stdlib only)
|
|
||||||
├─ ramble.py # GUI for first feature (optional)
|
|
||||||
├─ create_feature.py # CLI tool for creating features
|
|
||||||
├─ assets/ # Embedded resources
|
|
||||||
│ ├─ hooks/
|
|
||||||
│ │ └─ pre-commit # Pre-commit hook template
|
|
||||||
│ └─ templates/ # All templates from source assets/
|
|
||||||
│ ├─ USER_GUIDE.md
|
|
||||||
│ ├─ feature_request.md
|
│ ├─ feature_request.md
|
||||||
│ ├─ feature.discussion.md
|
│ ├─ discussion.md
|
||||||
│ ├─ feature.discussion.sum.md
|
|
||||||
│ ├─ design_doc.md
|
│ ├─ design_doc.md
|
||||||
│ ├─ root_gitignore
|
│ └─ implementation_plan.md
|
||||||
│ ├─ process/
|
├─ Docs/
|
||||||
│ │ └─ policies.yml
|
│ ├─ features/
|
||||||
│ └─ rules/
|
│ │ ├─ .ai-rules.yml # Folder-scoped rules for all features
|
||||||
│ ├─ root.ai-rules.yml
|
│ │ ├─ FR_YYYY-MM-DD_<slug>/ # Individual feature folders
|
||||||
│ └─ features.ai-rules.yml
|
│ │ │ ├─ request.md # Original feature request
|
||||||
├─ INSTALL.md # Bundle-local instructions
|
│ │ │ ├─ discussions/ # Stage-specific conversations
|
||||||
└─ VERSION # Version metadata
|
│ │ │ │ ├─ 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
|
||||||
**Purpose:** End-user distribution. Can be zipped and shared. Requires only Python 3.10+ stdlib (PySide6 optional for GUI).
|
│ │ │ │ ├─ implementation.discussion.md # Track implementation
|
||||||
|
│ │ │ │ ├─ implementation.discussion.sum.md # Summary of the implementation discussion
|
||||||
**Rationale:** Minimal, auditable, portable. No external dependencies for core functionality. Users can inspect all files before running.
|
│ │ │ │ ├─ 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
|
||||||
### C) User Project (Generated by Installer)
|
│ │ │ ├─ design/ # Design artifacts
|
||||||
|
│ │ │ │ ├─ design.md # Evolving design document
|
||||||
This is the structure created when a user runs `setup_cascadingdev.py --target /path/to/project`.
|
│ │ │ │ └─ diagrams/ # Architecture diagrams
|
||||||
|
│ │ │ ├─ implementation/ # Implementation artifacts
|
||||||
```text
|
│ │ │ │ ├─ plan.md # Implementation plan
|
||||||
my-project/ # User's application repository
|
│ │ │ │ └─ tasks.md # Task checklist
|
||||||
├─ .git/ # Git repository
|
│ │ │ ├─ testing/ # Testing artifacts
|
||||||
│ └─ hooks/
|
│ │ │ │ ├─ testplan.md # Test strategy
|
||||||
│ └─ pre-commit # Installed automatically from bundle
|
│ │ │ │ └─ checklist.md # Test checklist
|
||||||
├─ .gitignore # Generated from root_gitignore template
|
│ │ │ ├─ review/ # Review artifacts
|
||||||
├─ .ai-rules.yml # Root cascading rules (from templates/rules/)
|
│ │ │ │ └─ findings.md # Feature-specific review findings
|
||||||
├─ USER_GUIDE.md # Daily workflow reference
|
│ │ │ └─ bugs/ # Auto-generated bug reports
|
||||||
├─ ramble.py # Copied from bundle (optional GUI helper)
|
│ │ │ └─ BUG_YYYYMMDD_<slug>/
|
||||||
├─ create_feature.py # Copied from bundle (CLI tool)
|
│ │ │ ├─ report.md
|
||||||
├─ Docs/ # Documentation and feature tracking
|
│ │ │ ├─ discussion.md
|
||||||
│ ├─ features/ # All features live here
|
│ │ │ └─ fix/
|
||||||
│ │ ├─ .ai-rules.yml # Feature-level cascading rules
|
│ │ │ ├─ plan.md
|
||||||
│ │ └─ FR_YYYY-MM-DD_<slug>/ # Individual feature folders
|
│ │ │ └─ tasks.md
|
||||||
│ │ ├─ request.md # Original feature request
|
│ ├─ discussions/
|
||||||
│ │ └─ discussions/ # Stage-specific conversation threads
|
│ │ └─ reviews/ # Code reviews from hook
|
||||||
│ │ ├─ feature.discussion.md # Feature discussion
|
│ └─ diagrams/
|
||||||
│ │ ├─ feature.discussion.sum.md # Auto-maintained summary
|
│ └─ file_diagrams/ # PlantUML from source files
|
||||||
│ │ ├─ design.discussion.md # Design discussion
|
├─ src/ # Application source code
|
||||||
│ │ ├─ design.discussion.sum.md # Auto-maintained summary
|
└─ tests/ # System test suite
|
||||||
│ │ ├─ implementation.discussion.md # Implementation tracking
|
|
||||||
│ │ ├─ implementation.discussion.sum.md
|
|
||||||
│ │ ├─ testing.discussion.md # Test planning
|
|
||||||
│ │ ├─ testing.discussion.sum.md
|
|
||||||
│ │ ├─ review.discussion.md # Final review
|
|
||||||
│ │ └─ review.discussion.sum.md
|
|
||||||
│ │ ├─ design/ # Design artifacts (created during design stage)
|
|
||||||
│ │ │ ├─ design.md # Evolving design document
|
|
||||||
│ │ │ └─ diagrams/ # Architecture diagrams
|
|
||||||
│ │ ├─ implementation/ # Implementation artifacts
|
|
||||||
│ │ │ ├─ plan.md # Implementation plan
|
|
||||||
│ │ │ └─ tasks.md # Task checklist
|
|
||||||
│ │ ├─ testing/ # Testing artifacts
|
|
||||||
│ │ │ ├─ testplan.md # Test strategy
|
|
||||||
│ │ │ └─ checklist.md # Test checklist
|
|
||||||
│ │ ├─ review/ # Review artifacts
|
|
||||||
│ │ │ └─ findings.md # Code review findings
|
|
||||||
│ │ └─ bugs/ # Bug sub-cycles (future)
|
|
||||||
│ │ └─ BUG_YYYYMMDD_<slug>/
|
|
||||||
│ │ ├─ report.md
|
|
||||||
│ │ ├─ discussion.md
|
|
||||||
│ │ └─ fix/
|
|
||||||
│ │ ├─ plan.md
|
|
||||||
│ │ └─ tasks.md
|
|
||||||
│ ├─ discussions/ # Global discussions (future)
|
|
||||||
│ │ └─ reviews/ # Code reviews from hook
|
|
||||||
│ └─ diagrams/ # Auto-generated diagrams (future)
|
|
||||||
│ └─ file_diagrams/ # PlantUML from source files
|
|
||||||
├─ process/ # Process configuration
|
|
||||||
│ ├─ policies.yml # Machine-readable policies (voting, gates)
|
|
||||||
│ └─ templates/ # Local template overrides (optional)
|
|
||||||
├─ src/ # User's application source code
|
|
||||||
│ └─ (user's code)
|
|
||||||
└─ tests/ # User's test suite
|
|
||||||
├─ unit/
|
├─ unit/
|
||||||
└─ integration/
|
├─ integration/
|
||||||
|
└─ bin/
|
||||||
FUTURE (not currently created, planned for M1+):
|
|
||||||
├─ automation/ # 🚧 M1: Orchestration layer
|
|
||||||
│ ├─ workflow.py # Vote parsing, status reporting
|
|
||||||
│ ├─ adapters/ # Model and platform integrations
|
|
||||||
│ └─ agents.yml # Agent role configuration
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Purpose:** This is the user's actual project repository where they develop their application while using the CascadingDev workflow.
|
|
||||||
|
|
||||||
**Key Points:**
|
The sections below describe the meta-infrastructure of CascadingDev itself — how it builds and distributes the installer that generates user projects.
|
||||||
- The first feature request defines the entire project's purpose
|
|
||||||
- All discussions are version-controlled alongside code
|
|
||||||
- Pre-commit hook maintains summary files automatically
|
|
||||||
- Templates can be overridden locally in `process/templates/`
|
|
||||||
- The `automation/` directory is planned but not yet implemented (M1)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Installation & Distribution Architecture
|
## Installation & Distribution Architecture
|
||||||
|
|
||||||
### First-Run Flow (User's Project Initialization)
|
### Terminology (clarified)
|
||||||
|
- **CascadingDev** — this tooling project (the code in this repository).
|
||||||
|
- **User’s project** — a new repository scaffolded by running CascadingDev’s installer.
|
||||||
|
- **Install bundle** — the small, distributable folder produced by the build process (unzipped and executed by end users).
|
||||||
|
|
||||||
|
|
||||||
|
### Repository Layout (authoritative)
|
||||||
|
|
||||||
|
Note: This section refers to the CascadingDev repository itself. For the structure of a user’s generated project, see “First-Run Flow” below.
|
||||||
|
```text
|
||||||
|
CascadingDev/
|
||||||
|
├─ src/cascadingdev/ # core logic & optional dev CLI
|
||||||
|
├─ assets/ # single source of truth for shipped files
|
||||||
|
│ ├─ hooks/pre-commit
|
||||||
|
│ ├─ templates/{feature_request.md,discussion.md,design_doc.md}
|
||||||
|
│ └─ runtime/ramble.py
|
||||||
|
├─ tools/build_installer.py # creates install/cascadingdev-<version>/
|
||||||
|
├─ install/ # build output (git-ignored)
|
||||||
|
│ └─ cascadingdev-<version>/ # unzip + run bundle (setup_cascadingdev.py inside)
|
||||||
|
├─ VERSION # semantic version of CascadingDev
|
||||||
|
├─ DESIGN.md, README.md, docs/, tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** All runtime assets live once under `assets/`.
|
||||||
|
The builder copies only what end users need into a clean bundle.
|
||||||
|
Development happens in `src/` and is testable; distribution is “unzip + run”.
|
||||||
|
|
||||||
|
|
||||||
|
### Install Bundle Specification
|
||||||
|
|
||||||
|
Contents of `install/cascadingdev-<version>/`:
|
||||||
|
|
||||||
|
- `setup_cascadingdev.py` — single-file installer (stdlib-only)
|
||||||
|
- `DESIGN.md` — copied for user reference
|
||||||
|
- `ramble.py` — GUI dialog for first feature request (PySide6/PyQt5)
|
||||||
|
- `assets/hooks/pre-commit` — git pre-commit template (executable)
|
||||||
|
- `assets/templates/*.md` — feature/discussion/design templates
|
||||||
|
- `VERSION` — set from repo root `VERSION`
|
||||||
|
|
||||||
|
**Rationale:** Minimal, auditable, portable; no local package imports required.
|
||||||
|
|
||||||
|
|
||||||
|
### First-Run Flow (User’s Project Initialization)
|
||||||
|
|
||||||
User runs:
|
User runs:
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -258,12 +180,10 @@ python setup_cascadingdev.py --target /path/to/users-project [--no-ramble] [--pr
|
||||||
```
|
```
|
||||||
|
|
||||||
**Installer actions:**
|
**Installer actions:**
|
||||||
- Creates standard folders (Docs/, process/templates/, etc.)
|
- Creates standard folders (`Docs/`, `process/templates/`, etc.)
|
||||||
- Copies templates, ramble.py, and create_feature.py into the user project
|
- Copies templates, `ramble.py`, `DESIGN.md`, and installs pre-commit hook
|
||||||
- Initializes git (main branch), writes `.gitignore`
|
- Initializes git (main branch), writes `.gitignore`
|
||||||
- Installs pre-commit hook
|
- Launches Ramble (unless `--no-ramble`) to collect the first Feature Request
|
||||||
- Optionally launches Ramble (unless --no-ramble) to help collect first Feature Request
|
|
||||||
- Writes a concise USER_GUIDE.md into the user project root for day-to-day use
|
|
||||||
|
|
||||||
**Seeds:**
|
**Seeds:**
|
||||||
```
|
```
|
||||||
|
|
@ -276,7 +196,6 @@ Initial commit message: “bootstrap Cascading Development scaffolding”.
|
||||||
|
|
||||||
**Fallback:** If Ramble JSON isn’t returned, installer prints to stderr and optionally falls back to terminal prompts.
|
**Fallback:** If Ramble JSON isn’t returned, installer prints to stderr and optionally falls back to terminal prompts.
|
||||||
|
|
||||||
Important: The CascadingDev DESIGN.md is not copied into user projects. The first feature’s design doc (created later at the design stage) becomes the project’s own design document.
|
|
||||||
|
|
||||||
### Pre-Commit Hook (v1 behavior)
|
### Pre-Commit Hook (v1 behavior)
|
||||||
- Fast regex secret scan on staged diffs
|
- 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.
|
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)
|
### Build & Release Process (repeatable)
|
||||||
|
|
||||||
Goal: deterministic “unzip + run” artifact for each version.
|
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**
|
**6.1 Versioning**
|
||||||
- Update `VERSION` (semver): `MAJOR.MINOR.PATCH`
|
- Update `VERSION` (semver): `MAJOR.MINOR.PATCH`
|
||||||
- Tag releases in git to match `VERSION`
|
- Tag releases in git to match `VERSION`
|
||||||
|
|
@ -837,6 +315,7 @@ python setup_cascadingdev.py --target /path/to/users-project
|
||||||
6. Makes initial commit
|
6. Makes initial commit
|
||||||
|
|
||||||
If GUI fails, use a virtualenv and \`pip install PySide6\`, or run with \`--no-ramble\`.
|
If GUI fails, use a virtualenv and \`pip install PySide6\`, or run with \`--no-ramble\`.
|
||||||
|
```
|
||||||
|
|
||||||
This ensures every distributed bundle includes explicit usage instructions.
|
This ensures every distributed bundle includes explicit usage instructions.
|
||||||
|
|
||||||
|
|
@ -1343,7 +822,7 @@ rules:
|
||||||
feature_request:
|
feature_request:
|
||||||
outputs:
|
outputs:
|
||||||
feature_discussion:
|
feature_discussion:
|
||||||
path: "{dir}/discussions/feature.feature.discussion.md"
|
path: "{dir}/discussions/feature.discussion.md"
|
||||||
output_type: "feature_discussion_writer"
|
output_type: "feature_discussion_writer"
|
||||||
instruction: |
|
instruction: |
|
||||||
If missing: create with standard header (stage: feature, status: OPEN),
|
If missing: create with standard header (stage: feature, status: OPEN),
|
||||||
|
|
@ -1363,7 +842,7 @@ rules:
|
||||||
outputs:
|
outputs:
|
||||||
# 1) Append the new AI comment to the discussion (append-only)
|
# 1) Append the new AI comment to the discussion (append-only)
|
||||||
self_append:
|
self_append:
|
||||||
path: "{dir}/discussions/feature.feature.discussion.md"
|
path: "{dir}/discussions/feature.discussion.md"
|
||||||
output_type: "feature_discussion_writer"
|
output_type: "feature_discussion_writer"
|
||||||
instruction: |
|
instruction: |
|
||||||
Append concise comment signed with AI name, ending with a single vote line.
|
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
|
# 3) Promotion artifacts when READY_FOR_DESIGN
|
||||||
design_discussion:
|
design_discussion:
|
||||||
path: "{dir}/discussions/design.feature.discussion.md"
|
path: "{dir}/discussions/design.discussion.md"
|
||||||
output_type: "design_discussion_writer"
|
output_type: "design_discussion_writer"
|
||||||
instruction: |
|
instruction: |
|
||||||
Create ONLY if feature discussion status is READY_FOR_DESIGN.
|
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.
|
Update only the marker-bounded sections from the discussion content.
|
||||||
|
|
||||||
impl_discussion:
|
impl_discussion:
|
||||||
path: "{dir}/discussions/implementation.feature.discussion.md"
|
path: "{dir}/discussions/implementation.discussion.md"
|
||||||
output_type: "impl_discussion_writer"
|
output_type: "impl_discussion_writer"
|
||||||
instruction: |
|
instruction: |
|
||||||
Create ONLY if design discussion status is READY_FOR_IMPLEMENTATION.
|
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.
|
Include unchecked items from ../implementation/tasks.md in ACTION_ITEMS.
|
||||||
|
|
||||||
test_discussion:
|
test_discussion:
|
||||||
path: "{dir}/discussions/testing.feature.discussion.md"
|
path: "{dir}/discussions/testing.discussion.md"
|
||||||
output_type: "test_discussion_writer"
|
output_type: "test_discussion_writer"
|
||||||
instruction: |
|
instruction: |
|
||||||
Create ONLY if implementation status is READY_FOR_TESTING.
|
Create ONLY if implementation status is READY_FOR_TESTING.
|
||||||
|
|
@ -1517,7 +996,7 @@ rules:
|
||||||
Initialize bug discussion and fix plan in the same folder.
|
Initialize bug discussion and fix plan in the same folder.
|
||||||
|
|
||||||
review_discussion:
|
review_discussion:
|
||||||
path: "{dir}/discussions/review.feature.discussion.md"
|
path: "{dir}/discussions/review.discussion.md"
|
||||||
output_type: "review_discussion_writer"
|
output_type: "review_discussion_writer"
|
||||||
instruction: |
|
instruction: |
|
||||||
Create ONLY if all test checklist items pass.
|
Create ONLY if all test checklist items pass.
|
||||||
|
|
@ -1634,7 +1113,7 @@ resolve_template() {
|
||||||
ext="${basename##*.}"
|
ext="${basename##*.}"
|
||||||
# nearest FR_* ancestor as feature_id
|
# nearest FR_* ancestor as feature_id
|
||||||
feature_id="$(echo "$rel_path" | sed -n 's|.*Docs/features/\(FR_[^/]*\).*|\1|p')"
|
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')"
|
stage="$(echo "$basename" | sed -n 's/^\([A-Za-z0-9_-]\+\)\.discussion\.md$/\1/p')"
|
||||||
echo "$tmpl" \
|
echo "$tmpl" \
|
||||||
| sed -e "s|{date}|$today|g" \
|
| sed -e "s|{date}|$today|g" \
|
||||||
|
|
@ -1781,7 +1260,7 @@ Rule Definition (in Docs/features/.ai-rules.yml):
|
||||||
discussion_moderator_nudge:
|
discussion_moderator_nudge:
|
||||||
outputs:
|
outputs:
|
||||||
self_append:
|
self_append:
|
||||||
path: "{dir}/discussions/{stage}.feature.discussion.md"
|
path: "{dir}/discussions/{stage}.discussion.md"
|
||||||
output_type: "discussion_moderator_writer"
|
output_type: "discussion_moderator_writer"
|
||||||
instruction: |
|
instruction: |
|
||||||
Act as AI_Moderator. Analyze the entire discussion and:
|
Act as AI_Moderator. Analyze the entire discussion and:
|
||||||
|
|
@ -1918,7 +1397,7 @@ Bypass & Minimal Patch:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
.git/ai-rules-debug/
|
.git/ai-rules-debug/
|
||||||
├─ 20251021-143022-12345-feature.feature.discussion.md/
|
├─ 20251021-143022-12345-feature.discussion.md/
|
||||||
│ ├─ raw.out # Raw model output
|
│ ├─ raw.out # Raw model output
|
||||||
│ ├─ clean.diff # Extracted patch
|
│ ├─ clean.diff # Extracted patch
|
||||||
│ ├─ sanitized.diff # After sanitization
|
│ ├─ sanitized.diff # After sanitization
|
||||||
|
|
@ -2468,7 +1947,7 @@ Docs/features/FR_.../
|
||||||
type: discussion-summary
|
type: discussion-summary
|
||||||
stage: feature # feature|design|implementation|testing|review
|
stage: feature # feature|design|implementation|testing|review
|
||||||
status: ACTIVE # ACTIVE|SNAPSHOT|ARCHIVED
|
status: ACTIVE # ACTIVE|SNAPSHOT|ARCHIVED
|
||||||
source_discussion: feature.feature.discussion.md
|
source_discussion: feature.discussion.md
|
||||||
feature_id: FR_YYYY-MM-DD_<slug>
|
feature_id: FR_YYYY-MM-DD_<slug>
|
||||||
updated: YYYY-MM-DDTHH:MM:SSZ
|
updated: YYYY-MM-DDTHH:MM:SSZ
|
||||||
policy:
|
policy:
|
||||||
|
|
@ -2714,17 +2193,13 @@ Process Overhead:
|
||||||
- Flexibility: Bypass options for trivial changes
|
- Flexibility: Bypass options for trivial changes
|
||||||
|
|
||||||
## Initial Setup & Bootstrapping
|
## Initial Setup & Bootstrapping
|
||||||
To streamline project onboarding and ensure every repository begins with a structured, traceable starting point, this system includes:
|
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.
|
||||||
- a one-time setup script (`setup_cascadingdev.py`) that initializes the folder structure and installs the hook,
|
|
||||||
- a `create_feature.py` tool for creating feature requests (with or without Ramble),
|
|
||||||
- and a concise `USER_GUIDE.md` in the user project for daily guidance.
|
|
||||||
|
|
||||||
### Steps Performed:
|
### Steps Performed:
|
||||||
- Create the canonical folder structure under `Docs/` and seed the initial FR folder.
|
- Create the canonical folder structure under Docs/features/FR_<date>_initial-feature-request/, including the request.md template.
|
||||||
- Install the pre-commit hook and default configuration files.
|
- Run the interactive dialog utility to guide the user (or team) through describing the project’s intent, motivation, and constraints in natural language.
|
||||||
- Copy `create_feature.py` and (optionally) `ramble.py` into the user project root.
|
- Initialize Git hooks, orchestration scripts, and default configuration files.
|
||||||
- Optionally run Ramble to help collect the first feature; otherwise prompt via CLI.
|
- Automatically generate the first Feature Request document from that conversation.
|
||||||
- Generate the first Feature Request folder and the initial discussion + summary.
|
|
||||||
|
|
||||||
Example Implementation
|
Example Implementation
|
||||||
```python
|
```python
|
||||||
|
|
@ -2757,7 +2232,7 @@ def main():
|
||||||
# Run Ramble dialog to fill in details interactively
|
# Run Ramble dialog to fill in details interactively
|
||||||
print("Launching Ramble interactive prompt...")
|
print("Launching Ramble interactive prompt...")
|
||||||
run_ramble()
|
run_ramble()
|
||||||
print("Setup complete — run create_feature.py to add more features.")
|
print("Setup complete — initial feature request created.")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
@ -2772,10 +2247,6 @@ Template Location as Version:
|
||||||
- Breaking changes require new feature request and migration plan
|
- Breaking changes require new feature request and migration plan
|
||||||
- Existing features use templates current at their creation
|
- Existing features use templates current at their creation
|
||||||
|
|
||||||
**User Guide**
|
|
||||||
- The authoritative `USER_GUIDE.md` lives in CascadingDev’s source (`assets/templates/USER_GUIDE.md`) and is copied
|
|
||||||
into user projects (root) at install time. Update the source and rebuild the bundle to propagate changes.
|
|
||||||
|
|
||||||
Migration Guidance:
|
Migration Guidance:
|
||||||
- Document template changes in release notes
|
- Document template changes in release notes
|
||||||
- Provide automated migration scripts for simple changes
|
- Provide automated migration scripts for simple changes
|
||||||
76
README.md
76
README.md
|
|
@ -1,76 +0,0 @@
|
||||||
# CascadingDev (CDev) - Simplified
|
|
||||||
|
|
||||||
**CDev** — short for *Cascading Development* — is a **Git-native AI–human collaboration framework** that uses git hooks and cascading rules to enhance your development workflow.
|
|
||||||
|
|
||||||
This is the **simplified version** focused on core functionality:
|
|
||||||
- Git pre-commit hooks with safety checks
|
|
||||||
- Cascading `.ai-rules.yml` system
|
|
||||||
- Ramble GUI for capturing structured feature requests
|
|
||||||
|
|
||||||
For advanced discussion orchestration features, see [Orchestrated Discussions](https://gitea.brrd.tech/rob/orchestrated-discussions).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Key Features
|
|
||||||
|
|
||||||
- **Cascading Rules System** — nearest `.ai-rules.yml` defines behavior at each directory level
|
|
||||||
- **Pre-commit Hook** — secret scanning, discussion summary creation, git corruption prevention
|
|
||||||
- **Ramble GUI** — PySide6/PyQt5 dialog for capturing structured feature requests
|
|
||||||
- **Deterministic Builds** — reproducible installer bundle
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Create and activate a virtual environment
|
|
||||||
python3 -m venv .venv
|
|
||||||
source .venv/bin/activate
|
|
||||||
pip install --upgrade pip wheel PySide6
|
|
||||||
|
|
||||||
# 2. Build the installer bundle
|
|
||||||
python tools/build_installer.py
|
|
||||||
|
|
||||||
# 3. Install into a project folder
|
|
||||||
python install/cascadingdev-*/setup_cascadingdev.py --target /path/to/myproject
|
|
||||||
```
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
CascadingDev/
|
|
||||||
├── assets/
|
|
||||||
│ ├── hooks/pre-commit # Git pre-commit hook
|
|
||||||
│ ├── runtime/ # Runtime scripts (ramble.py, create_feature.py)
|
|
||||||
│ └── templates/ # Discussion and rule templates
|
|
||||||
├── src/cascadingdev/ # Python package
|
|
||||||
│ ├── setup_project.py # Project initialization
|
|
||||||
│ ├── cli.py # Command-line interface
|
|
||||||
│ └── ...
|
|
||||||
├── tools/ # Build and test tools
|
|
||||||
└── docs/ # Documentation
|
|
||||||
```
|
|
||||||
|
|
||||||
## Pre-commit Hook Features
|
|
||||||
|
|
||||||
The pre-commit hook provides:
|
|
||||||
|
|
||||||
1. **Secret Scanning** - Prevents accidental commit of API keys and secrets
|
|
||||||
2. **Summary Files** - Auto-creates `.sum.md` companion files for discussions
|
|
||||||
3. **Concurrency Safety** - Uses flock to prevent git corruption from parallel commits
|
|
||||||
|
|
||||||
Environment variables:
|
|
||||||
- `CDEV_SKIP_HOOK=1` - Skip all hook checks
|
|
||||||
- `CDEV_SKIP_SUMMARIES=1` - Skip summary file generation
|
|
||||||
|
|
||||||
## Related Projects
|
|
||||||
|
|
||||||
This project is part of a three-layer stack:
|
|
||||||
|
|
||||||
1. **[SmartTools](https://gitea.brrd.tech/rob/SmartTools)** - AI provider abstraction and tool execution
|
|
||||||
2. **[Orchestrated Discussions](https://gitea.brrd.tech/rob/orchestrated-discussions)** - Multi-agent discussion orchestration
|
|
||||||
3. **[Ramble](https://gitea.brrd.tech/rob/ramble)** - AI-powered structured field extraction GUI
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT
|
|
||||||
|
|
@ -1,59 +1,18 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
# Safety settings: exit on errors, treat unset variables as errors, and catch pipeline failures
|
||||||
# CascadingDev Pre-commit Hook
|
|
||||||
# =============================
|
|
||||||
# This hook provides safety checks during git commits.
|
|
||||||
#
|
|
||||||
# What it does:
|
|
||||||
# 1. Scans for potential secrets in staged changes
|
|
||||||
# 2. Creates companion summary files (.sum.md) for discussion files
|
|
||||||
#
|
|
||||||
# Environment Variables:
|
|
||||||
# CDEV_SKIP_HOOK=1 Skip all checks (hook exits immediately)
|
|
||||||
# CDEV_SKIP_SUMMARIES=1 Skip summary file generation
|
|
||||||
#
|
|
||||||
# Safety: Exits on errors to prevent broken commits
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
if [[ -n "${CDEV_SKIP_HOOK:-}" ]]; then
|
# 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
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Navigate to git repository root so all file paths work correctly
|
|
||||||
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo ".")"
|
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo ".")"
|
||||||
cd "$ROOT"
|
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 ----------
|
# -------- 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)
|
mapfile -t STAGED < <(git diff --cached --name-only --diff-filter=AM || true)
|
||||||
[ "${#STAGED[@]}" -eq 0 ] && exit 0
|
[ "${#STAGED[@]}" -eq 0 ] && exit 0
|
||||||
|
|
||||||
# -------- tiny secret scan (fast, regex only) ----------
|
# -------- 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)"
|
DIFF="$(git diff --cached)"
|
||||||
if echo "$DIFF" | grep -Eqi '(api[_-]?key|secret|access[_-]?token|private[_-]?key)[:=]\s*[A-Za-z0-9_\-]{12,}'; then
|
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."
|
echo >&2 "[pre-commit] Possible secret detected in staged changes."
|
||||||
|
|
@ -62,13 +21,13 @@ if echo "$DIFF" | grep -Eqi '(api[_-]?key|secret|access[_-]?token|private[_-]?ke
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# -------- ensure discussion summaries exist (companion files) ----------
|
# -------- 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() {
|
ensure_summary() {
|
||||||
local disc="$1"
|
local disc="$1"
|
||||||
local dir; dir="$(dirname "$disc")"
|
local dir; dir="$(dirname "$disc")"
|
||||||
local sum="$dir/$(basename "$disc" .md).sum.md"
|
local sum="$dir/$(basename "$disc" .md).sum.md"
|
||||||
if [ ! -f "$sum" ]; then
|
if [ ! -f "$sum" ]; then
|
||||||
cat > "$sum" <<'EOF'
|
cat > "$sum" <<'EOF'
|
||||||
# Summary — <Stage Title>
|
# Summary — <Stage Title>
|
||||||
|
|
||||||
<!-- SUMMARY:DECISIONS START -->
|
<!-- SUMMARY:DECISIONS START -->
|
||||||
|
|
@ -109,16 +68,21 @@ READY: 0 • CHANGES: 0 • REJECT: 0
|
||||||
- Design/Plan: ../design/design.md
|
- Design/Plan: ../design/design.md
|
||||||
<!-- SUMMARY:LINKS END -->
|
<!-- SUMMARY:LINKS END -->
|
||||||
EOF
|
EOF
|
||||||
git add "$sum"
|
git add "$sum"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Process each staged discussion file and ensure it has a summary
|
# Process each staged discussion file and ensure it has a summary
|
||||||
for f in "${STAGED[@]}"; do
|
for f in "${STAGED[@]}"; do
|
||||||
case "$f" in
|
case "$f" in
|
||||||
Docs/features/*/discussions/*.discussion.md) ensure_summary "$f";;
|
Docs/features/*/discussions/*.discussion.md) ensure_summary "$f";;
|
||||||
esac
|
esac
|
||||||
done
|
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
|
fi
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -699,7 +699,6 @@ def parse_args():
|
||||||
p.add_argument("--prompt", default="Explain your new feature idea")
|
p.add_argument("--prompt", default="Explain your new feature idea")
|
||||||
p.add_argument("--fields", nargs="+", default=["Summary","Title","Intent","ProblemItSolves","BriefOverview"])
|
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("--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("--timeout", type=int, default=90)
|
||||||
p.add_argument("--tail", type=int, default=6000)
|
p.add_argument("--tail", type=int, default=6000)
|
||||||
p.add_argument("--debug", action="store_true")
|
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)
|
print("[FATAL] 'requests' is required for image backends. pip install requests", file=sys.stderr)
|
||||||
sys.exit(3)
|
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(
|
demo = open_ramble_dialog(
|
||||||
prompt=args.prompt,
|
prompt=args.prompt,
|
||||||
fields=args.fields,
|
fields=args.fields,
|
||||||
field_criteria=criteria,
|
field_criteria=criteria,
|
||||||
hints = hints,
|
hints=None,
|
||||||
provider=provider,
|
provider=provider,
|
||||||
enable_stability=args.stability,
|
enable_stability=args.stability,
|
||||||
enable_pexels=args.pexels,
|
enable_pexels=args.pexels,
|
||||||
|
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
# Project Guide
|
|
||||||
|
|
||||||
## Core idea
|
|
||||||
- An **empty project’s first feature defines the whole project**.
|
|
||||||
Subsequent features extend that foundation.
|
|
||||||
|
|
||||||
## Daily flow
|
|
||||||
1. Create or update features under `Docs/features/FR_*/...`.
|
|
||||||
2. Commit. The pre-commit hook **drives the discussion** and **maintains summaries** (within marker blocks).
|
|
||||||
3. Discuss in `Docs/features/.../discussions/*.discussion.md` and **end each comment with**
|
|
||||||
`VOTE: READY` or `CHANGES` or `REJECT`.
|
|
||||||
|
|
||||||
## First run
|
|
||||||
- After installation, make an initial commit to activate the hook:
|
|
||||||
```bash
|
|
||||||
git add .
|
|
||||||
git commit -m "chore: initial commit"
|
|
||||||
```
|
|
||||||
## Start a new feature (recommended)
|
|
||||||
- Copy process/templates/feature_request.md to Docs/features/FR_YYYY-MM-DD_<slug>/request.md
|
|
||||||
- Fill in: Intent, Motivation, Constraints, Rough Proposal, Open Questions, Author
|
|
||||||
- Commit; the system will drive the discussion and generate/maintain summaries automatically.
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
- Keep discussions append-only; votes are single-line VOTE: markers.
|
|
||||||
- Human READY is required at Implementation/Release stages.
|
|
||||||
- Ramble (ramble.py) is optional; it can extract fields from your free-form notes.
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
<!--META
|
|
||||||
{
|
|
||||||
"kind": "discussion",
|
|
||||||
"tokens": ["FeatureId", "CreatedDate"]
|
|
||||||
}
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
Initial discussion for {FeatureId}. Append your comments below.
|
|
||||||
|
|
||||||
## Participation
|
|
||||||
- Maintainer: Kickoff. VOTE: READY
|
|
||||||
|
|
@ -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 -->
|
|
||||||
|
|
@ -1,35 +1,10 @@
|
||||||
# Feature Request: <title>
|
# Feature Request: <title>
|
||||||
|
|
||||||
<!--META
|
**Feature ID**: <FR_YYYY-MM-DD_slug>
|
||||||
{
|
**Intent**: <one paragraph describing purpose>
|
||||||
"kind": "feature_request",
|
**Motivation / Problem**: <why this is needed now>
|
||||||
"ramble_fields": [
|
**Constraints / Non-Goals**: <bulleted list of limitations>
|
||||||
{"name": "Title", "hint": "camelCase, ≤24 chars", "default": "initialProjectDesign"},
|
**Rough Proposal**: <short implementation outline>
|
||||||
{"name": "Intent"},
|
**Open Questions**: <bulleted list of uncertainties>
|
||||||
{"name": "ProblemItSolves"},
|
**Meta**: Created: <date> • Author: <name>
|
||||||
{"name": "BriefOverview"},
|
Discussion Template (process/templates/discussion.md):
|
||||||
{"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}
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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-*
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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
|
|
||||||
```
|
|
||||||
|
|
@ -1,21 +1,9 @@
|
||||||
# pyproject.toml
|
# pyproject.toml
|
||||||
[build-system]
|
|
||||||
requires = ["setuptools>=68", "wheel"]
|
|
||||||
build-backend = "setuptools.build_meta"
|
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "cascadingdev"
|
name = "cascadingdev"
|
||||||
# Tell PEP 621 that version is provided dynamically
|
version = "0.1.0"
|
||||||
dynamic = ["version"]
|
|
||||||
description = "CascadingDev: scaffold rule-driven multi-agent project repos"
|
description = "CascadingDev: scaffold rule-driven multi-agent project repos"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
cdev = "cascadingdev.cli:main"
|
cdev = "cascadingdev.cli:main"
|
||||||
|
|
||||||
[tool.setuptools]
|
|
||||||
package-dir = {"" = "src"}
|
|
||||||
packages = ["cascadingdev"]
|
|
||||||
|
|
||||||
[tool.setuptools.dynamic]
|
|
||||||
version = { file = "VERSION" }
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
# src/cascadingdev/__init__.py
|
|
||||||
from .utils import read_version
|
|
||||||
__all__ = ["cli"]
|
|
||||||
__version__ = read_version()
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -2,194 +2,85 @@
|
||||||
"""
|
"""
|
||||||
setup_project.py — Installer-mode bootstrap for Cascading Development
|
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
|
- 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
|
- Creates canonical structure, seeds rules/templates
|
||||||
- Initializes git and installs pre-commit hook
|
- Initializes git and installs pre-commit hook
|
||||||
- Launches Ramble to capture the first feature request
|
- Launches Ramble to capture the first feature request
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
python setup_cascadingdev.py --target ~/dev/my-new-repo
|
python3 scripts/setup_project.py
|
||||||
python setup_cascadingdev.py --target /abs/path --no-ramble
|
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 sys
|
||||||
import subprocess
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
|
import argparse
|
||||||
|
import subprocess
|
||||||
|
import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Bundle root (must contain assets/, ramble.py, VERSION)
|
INSTALL_ROOT = Path(__file__).resolve().parent.parent # installer root (contains this scripts/ dir)
|
||||||
INSTALL_ROOT = Path(__file__).resolve().parent
|
|
||||||
if not (INSTALL_ROOT / "assets").exists():
|
|
||||||
print("[-] This script must be run from the installer bundle directory (assets/ missing).")
|
|
||||||
print(
|
|
||||||
" Rebuild the bundle (e.g., `python tools/build_installer.py`) and run the copy in install/cascadingdev-*/.")
|
|
||||||
sys.exit(2)
|
|
||||||
|
|
||||||
|
# ---------- helpers ----------
|
||||||
|
def sh(cmd, check=True, cwd=None):
|
||||||
|
return subprocess.run(cmd, check=check, text=True, capture_output=True, cwd=cwd)
|
||||||
|
|
||||||
# ---------- Helper Functions ----------
|
def say(msg): print(msg, flush=True)
|
||||||
|
|
||||||
def say(msg: str) -> None:
|
def write_if_missing(path: Path, content: str):
|
||||||
print(msg, flush=True)
|
path.parent.mkdir(parents=True, exist_ok=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():
|
if not path.exists():
|
||||||
path.write_text(content, encoding="utf-8")
|
path.write_text(content, encoding="utf-8")
|
||||||
|
|
||||||
def copy_if_exists(src: Path, dst: Path) -> None:
|
def copy_if_exists(src: Path, dst: Path):
|
||||||
if src.exists():
|
if src.exists():
|
||||||
ensure_dir(dst.parent)
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
shutil.copy2(src, dst)
|
shutil.copy2(str(src), str(dst))
|
||||||
|
|
||||||
def copy_if_missing(src: Path, dst: Path) -> None:
|
|
||||||
ensure_dir(dst.parent)
|
|
||||||
if not dst.exists():
|
|
||||||
shutil.copy2(src, dst)
|
|
||||||
|
|
||||||
def run(cmd: list[str], cwd: Path | None = None) -> int:
|
|
||||||
proc = subprocess.Popen(cmd, cwd=cwd, stdout=sys.stdout, stderr=sys.stderr)
|
|
||||||
return proc.wait()
|
|
||||||
|
|
||||||
# --- Tiny template helpers ----------------------------------------------------
|
|
||||||
# Self-contained; no external dependencies
|
|
||||||
_META_RE = re.compile(r"<!--META\s*(\{.*?\})\s*-->", re.S)
|
|
||||||
|
|
||||||
def load_template_with_meta(path: Path) -> tuple[dict, str]:
|
|
||||||
"""
|
|
||||||
Returns (meta: dict, body_without_meta: str). If no META, ({} , full text).
|
|
||||||
META must be a single JSON object inside <!--META ... -->.
|
|
||||||
"""
|
|
||||||
if not path.exists():
|
|
||||||
return {}, ""
|
|
||||||
text = path.read_text(encoding="utf-8")
|
|
||||||
m = _META_RE.search(text)
|
|
||||||
if not m:
|
|
||||||
return {}, text
|
|
||||||
meta_json = m.group(1)
|
|
||||||
try:
|
|
||||||
meta = json.loads(meta_json)
|
|
||||||
except Exception:
|
|
||||||
meta = {}
|
|
||||||
body = _META_RE.sub("", text, count=1).lstrip()
|
|
||||||
return meta, body
|
|
||||||
|
|
||||||
def render_placeholders(body: str, values: dict) -> str:
|
|
||||||
"""
|
|
||||||
Simple {Token} replacement. Leaves unknown tokens as-is.
|
|
||||||
"""
|
|
||||||
# two-pass: {{Token}} then {Token}
|
|
||||||
out = body
|
|
||||||
for k, v in values.items():
|
|
||||||
out = out.replace("{{" + k + "}}", str(v))
|
|
||||||
try:
|
|
||||||
out = out.format_map({k: v for k, v in values.items()})
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return out
|
|
||||||
|
|
||||||
def meta_ramble_config(meta: dict) -> tuple[list[str], dict, dict, list[str]]:
|
|
||||||
"""
|
|
||||||
From template META, extract:
|
|
||||||
- fields: list of field names in order
|
|
||||||
- defaults: {field: default_value}
|
|
||||||
- criteria: {field: rule/description} (optional)
|
|
||||||
- hints: [str, ...] (optional)
|
|
||||||
"""
|
|
||||||
fields: list[str] = []
|
|
||||||
defaults: dict = {}
|
|
||||||
for spec in meta.get("ramble_fields", []):
|
|
||||||
name = spec.get("name")
|
|
||||||
if name:
|
|
||||||
fields.append(name)
|
|
||||||
if "default" in spec:
|
|
||||||
defaults[name] = spec["default"]
|
|
||||||
criteria = meta.get("criteria", {}) or {}
|
|
||||||
hints = meta.get("hints", []) or []
|
|
||||||
return fields, defaults, criteria, hints
|
|
||||||
|
|
||||||
def ensure_git_repo(target: Path):
|
def ensure_git_repo(target: Path):
|
||||||
"""Initialize a git repository if one doesn't exist at the target path."""
|
|
||||||
if not (target / ".git").exists():
|
if not (target / ".git").exists():
|
||||||
# Initialize git repo with main branch
|
sh(["git", "init", "-b", "main"], cwd=str(target))
|
||||||
run(["git", "init", "-b", "main"], cwd=target)
|
write_if_missing(target / ".gitignore", "\n".join([
|
||||||
# Seed .gitignore from template if present; otherwise fallback
|
".env", ".env.*", "secrets/", ".git/ai-rules-*", "__pycache__/",
|
||||||
tmpl_gitignore = INSTALL_ROOT / "assets" / "templates" / "root_gitignore"
|
"*.pyc", ".pytest_cache/", ".DS_Store",
|
||||||
if tmpl_gitignore.exists():
|
]) + "\n")
|
||||||
copy_if_missing(tmpl_gitignore, target / ".gitignore")
|
|
||||||
else:
|
|
||||||
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-*",
|
|
||||||
]) + "\n")
|
|
||||||
|
|
||||||
def install_precommit_hook(target: Path):
|
def install_precommit_hook(target: Path):
|
||||||
"""Install the pre-commit hook from installer assets to target git hooks."""
|
hook_src = INSTALL_ROOT / "assets" / "hooks" / "pre-commit"
|
||||||
hook_src = INSTALL_ROOT / "assets" / "hooks" / "pre-commit"
|
|
||||||
hooks_dir = target / ".git" / "hooks"
|
hooks_dir = target / ".git" / "hooks"
|
||||||
hooks_dir.mkdir(parents=True, exist_ok=True)
|
hooks_dir.mkdir(parents=True, exist_ok=True)
|
||||||
hook_dst = hooks_dir / "pre-commit"
|
hook_dst = hooks_dir / "pre-commit"
|
||||||
|
|
||||||
if not hook_src.exists():
|
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
|
return
|
||||||
|
|
||||||
# Copy hook content and make it executable
|
|
||||||
hook_dst.write_text(hook_src.read_text(encoding="utf-8"), encoding="utf-8")
|
hook_dst.write_text(hook_src.read_text(encoding="utf-8"), encoding="utf-8")
|
||||||
hook_dst.chmod(0o755)
|
hook_dst.chmod(0o755)
|
||||||
say(f"[+] Installed git hook → {hook_dst}")
|
say(f"[+] Installed git hook → {hook_dst}")
|
||||||
|
|
||||||
|
|
||||||
def run_ramble_and_collect(target: Path, provider: str = "mock", claude_cmd: str = "claude"):
|
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"
|
ramble = target / "ramble.py"
|
||||||
if not ramble.exists():
|
if not ramble.exists():
|
||||||
say("[-] ramble.py not found in target; skipping interactive FR capture.")
|
say("[-] ramble.py not found in target; skipping interactive FR capture.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Build Ramble arguments dynamically from the template-defined fields
|
|
||||||
args = [
|
args = [
|
||||||
sys.executable, str(ramble),
|
sys.executable, str(ramble),
|
||||||
"--provider", provider,
|
"--provider", provider,
|
||||||
"--claude-cmd", claude_cmd,
|
"--claude-cmd", claude_cmd,
|
||||||
"--prompt", "Describe your initial feature request for this repository",
|
"--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)…")
|
say("[•] Launching Ramble (close the dialog with Submit to return JSON)…")
|
||||||
proc = subprocess.run(args, text=True, capture_output=True, cwd=str(target))
|
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():
|
if proc.stderr and proc.stderr.strip():
|
||||||
say("[Ramble stderr]")
|
say("[Ramble stderr]")
|
||||||
say(proc.stderr.strip())
|
say(proc.stderr.strip())
|
||||||
|
|
||||||
# Try to parse JSON output from Ramble
|
|
||||||
out = (proc.stdout or "").strip()
|
out = (proc.stdout or "").strip()
|
||||||
if out:
|
if out:
|
||||||
try:
|
try:
|
||||||
|
|
@ -197,9 +88,8 @@ def run_ramble_and_collect(target: Path, provider: str = "mock", claude_cmd: str
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
say(f"[-] JSON parse failed: {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.")
|
say("[!] Falling back to terminal prompts.")
|
||||||
|
|
||||||
def ask(label, default=""):
|
def ask(label, default=""):
|
||||||
try:
|
try:
|
||||||
v = input(f"{label}: ").strip()
|
v = input(f"{label}: ").strip()
|
||||||
|
|
@ -207,111 +97,129 @@ def run_ramble_and_collect(target: Path, provider: str = "mock", claude_cmd: str
|
||||||
except EOFError:
|
except EOFError:
|
||||||
return default
|
return default
|
||||||
|
|
||||||
# Collect required fields via terminal input
|
|
||||||
fields = {
|
fields = {
|
||||||
"Title": ask("Title (camelCase, <=24 chars)", "initialProjectDesign"),
|
"Title": ask("Title (camelCase, <=24 chars)", "initialProjectDesign"),
|
||||||
"Intent": ask("Intent", "—"),
|
"Intent": ask("Intent", "—"),
|
||||||
"ProblemItSolves": ask("Problem it solves", "—"),
|
"ProblemItSolves": ask("Problem it solves", "—"),
|
||||||
"BriefOverview": ask("Brief overview", "—"),
|
"BriefOverview": ask("Brief overview", "—"),
|
||||||
"Summary": ask("One- or two-sentence summary", ""),
|
"Summary": ask("One- or two-sentence summary", ""),
|
||||||
}
|
}
|
||||||
return {"fields": fields, "summary": fields.get("Summary", "")}
|
return {"fields": fields, "summary": fields.get("Summary", "")}
|
||||||
|
|
||||||
|
|
||||||
def seed_process_and_rules(target: Path):
|
def seed_process_and_rules(target: Path):
|
||||||
"""Seed machine-readable policies and stage rules by copying installer templates."""
|
write_if_missing(target / "process" / "design.md",
|
||||||
# Seed process/policies.yml (machine-readable), per DESIGN.md Appendix A
|
"# Process & Architecture (Local Notes)\n\n(See DESIGN.md for full spec.)\n")
|
||||||
process_dir = target / "process"
|
write_if_missing(target / "process" / "policies.md",
|
||||||
rules_dir = target / "Docs" / "features"
|
"# Policies (Human-readable)\n\nSee machine-readable config in policies.yml.\n")
|
||||||
process_dir.mkdir(parents=True, exist_ok=True)
|
write_if_missing(target / "process" / "policies.yml",
|
||||||
rules_dir.mkdir(parents=True, exist_ok=True)
|
"""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
|
tmpl_dir = target / "process" / "templates"
|
||||||
t_root = INSTALL_ROOT / "assets" / "templates"
|
write_if_missing(tmpl_dir / "feature_request.md",
|
||||||
t_process = t_root / "process" / "policies.yml"
|
"# Feature Request: <title>\n\n**Intent**: …\n**Motivation / Problem**: …\n**Constraints / Non-Goals**: …\n**Rough Proposal**: …\n**Open Questions**: …\n")
|
||||||
t_rules_root = t_root / "rules" / "root.ai-rules.yml"
|
write_if_missing(tmpl_dir / "discussion.md",
|
||||||
t_rules_features = t_root / "rules" / "features.ai-rules.yml"
|
"---\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
|
write_if_missing(target / ".ai-rules.yml",
|
||||||
if t_process.exists():
|
"""version: 1
|
||||||
copy_if_missing(t_process, process_dir / "policies.yml")
|
file_associations:
|
||||||
|
"*.md": "md-file"
|
||||||
|
|
||||||
# Copy rules files into expected locations
|
rules:
|
||||||
# Root rules (optional if you want a project-wide baseline)
|
md-file:
|
||||||
if t_rules_root.exists():
|
description: "Normalize Markdown"
|
||||||
copy_if_missing(t_rules_root, target / ".ai-rules.yml")
|
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)
|
write_if_missing(target / "Docs" / "features" / ".ai-rules.yml",
|
||||||
if t_rules_features.exists():
|
"""version: 1
|
||||||
copy_if_missing(t_rules_features, rules_dir / ".ai-rules.yml")
|
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):
|
def seed_initial_feature(target: Path, req_fields: dict | None):
|
||||||
today = datetime.date.today().isoformat()
|
today = datetime.date.today().isoformat()
|
||||||
feature_id = f"FR_{today}_initial-feature-request"
|
fr_dir = target / "Docs" / "features" / f"FR_{today}_initial-feature-request"
|
||||||
fr_dir = target / "Docs" / "features" / feature_id
|
|
||||||
disc_dir = fr_dir / "discussions"
|
disc_dir = fr_dir / "discussions"
|
||||||
disc_dir.mkdir(parents=True, exist_ok=True)
|
disc_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Gather values from Ramble result (if any)
|
if req_fields:
|
||||||
fields = (req_fields or {}).get("fields", {}) if req_fields else {}
|
title = (req_fields.get("fields", {}) or {}).get("Title", "").strip() or "initialProjectDesign"
|
||||||
# Load FR template + META
|
intent = (req_fields.get("fields", {}) or {}).get("Intent", "").strip() or "—"
|
||||||
fr_tmpl = INSTALL_ROOT / "assets" / "templates" / "feature_request.md"
|
problem = (req_fields.get("fields", {}) or {}).get("ProblemItSolves", "").strip() or "—"
|
||||||
fr_meta, fr_body = load_template_with_meta(fr_tmpl)
|
brief = (req_fields.get("fields", {}) or {}).get("BriefOverview", "").strip() or "—"
|
||||||
field_names, defaults, _criteria, _hints = meta_ramble_config(fr_meta)
|
summary = (req_fields.get("summary") or "").strip()
|
||||||
|
body = f"""# Feature Request: {title}
|
||||||
# Build values map with defaults → ramble fields → system tokens
|
|
||||||
values = {}
|
|
||||||
values.update(defaults) # template defaults
|
|
||||||
values.update(fields) # user-entered
|
|
||||||
values.update({ # system tokens
|
|
||||||
"FeatureId": feature_id,
|
|
||||||
"CreatedDate": today,
|
|
||||||
})
|
|
||||||
|
|
||||||
# If no template body, fall back to your old default
|
|
||||||
if not fr_body.strip():
|
|
||||||
title = values.get("Title", "initialProjectDesign")
|
|
||||||
intent = values.get("Intent", "—")
|
|
||||||
problem = values.get("ProblemItSolves", "—")
|
|
||||||
brief = values.get("BriefOverview", "—")
|
|
||||||
summary = values.get("Summary", "")
|
|
||||||
fr_body = f"""# Feature Request: {title}
|
|
||||||
|
|
||||||
**Intent**: {intent}
|
**Intent**: {intent}
|
||||||
**Motivation / Problem**: {problem}
|
**Motivation / Problem**: {problem}
|
||||||
**Brief Overview**: {brief}
|
**Brief Overview**: {brief}
|
||||||
|
|
||||||
**Summary**: {summary}
|
**Summary**: {summary}
|
||||||
|
|
||||||
**Meta**: Created: {today}
|
**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:
|
else:
|
||||||
# your existing static content
|
body = (target / "process" / "templates" / "feature_request.md").read_text(encoding="utf-8")
|
||||||
(disc_dir / "feature.discussion.sum.md").write_text(
|
|
||||||
"""# Summary — Feature
|
(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
|
||||||
|
|
||||||
<!-- SUMMARY:DECISIONS START -->
|
<!-- SUMMARY:DECISIONS START -->
|
||||||
## Decisions (ADR-style)
|
## Decisions (ADR-style)
|
||||||
|
|
@ -350,49 +258,33 @@ READY: 1 • CHANGES: 0 • REJECT: 0
|
||||||
<!-- SUMMARY:LINKS END -->
|
<!-- SUMMARY:LINKS END -->
|
||||||
""".replace("{ts}", today), encoding="utf-8")
|
""".replace("{ts}", today), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
def copy_install_assets_to_target(target: Path):
|
def copy_install_assets_to_target(target: Path):
|
||||||
"""Copy essential files from the installer to the target repository."""
|
# Copy DESIGN.md and ramble.py from installer if present
|
||||||
# Runtime helpers into project root
|
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 / "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)
|
# Copy shipped templates (preferred source of truth)
|
||||||
tmpl_src = INSTALL_ROOT / "assets" / "templates"
|
tmpl_src = INSTALL_ROOT / "assets" / "templates"
|
||||||
if tmpl_src.exists():
|
if tmpl_src.exists():
|
||||||
shutil.copytree(tmpl_src, target / "process" / "templates", dirs_exist_ok=True)
|
shutil.copytree(tmpl_src, target / "process" / "templates", dirs_exist_ok=True)
|
||||||
|
|
||||||
# Place USER_GUIDE.md under process/ (clear separation from source templates)
|
# Copy the hook (you already install it to .git/hooks via install_precommit_hook)
|
||||||
ug_src = tmpl_src / "USER_GUIDE.md"
|
# If you ever want the raw hook inside the user's repo too:
|
||||||
if ug_src.exists():
|
# copy_if_exists(INSTALL_ROOT / "assets" / "hooks" / "pre-commit", target / "scripts" / "hooks" / "pre-commit")
|
||||||
(target / "process").mkdir(parents=True, exist_ok=True)
|
|
||||||
shutil.copy2(ug_src, target / "process" / "USER_GUIDE.md")
|
|
||||||
|
|
||||||
# Hook is installed into .git/hooks by install_precommit_hook()
|
|
||||||
|
|
||||||
# Optionally copy any additional assets you drop under installer/automation, etc.
|
# Optionally copy any additional assets you drop under installer/automation, etc.
|
||||||
# Example: copy starter automation folder if provided in installer
|
# Example: copy starter automation folder if provided in installer
|
||||||
if (INSTALL_ROOT / "automation").exists():
|
if (INSTALL_ROOT / "automation").exists():
|
||||||
shutil.copytree(INSTALL_ROOT / "automation", target / "automation", dirs_exist_ok=True)
|
shutil.copytree(INSTALL_ROOT / "automation", target / "automation", dirs_exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
def first_commit(target: Path):
|
def first_commit(target: Path):
|
||||||
"""Perform the initial git commit of all scaffolded files."""
|
|
||||||
try:
|
try:
|
||||||
run(["git", "add", "-A"], cwd=target)
|
sh(["git", "add", "-A"], cwd=str(target))
|
||||||
run(["git", "commit", "-m", "chore: bootstrap Cascading Development scaffolding"], cwd=target)
|
sh(["git", "commit", "-m", "chore: bootstrap Cascading Development scaffolding"], cwd=str(target))
|
||||||
except Exception:
|
except Exception:
|
||||||
# Silently continue if commit fails (e.g., no git config)
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Main entry point for the Cascading Development setup script."""
|
|
||||||
# Parse command line arguments
|
|
||||||
ap = argparse.ArgumentParser()
|
ap = argparse.ArgumentParser()
|
||||||
ap.add_argument("--target", help="Destination path to create/use the repo")
|
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)")
|
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")
|
ap.add_argument("--claude-cmd", default="claude")
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
||||||
# Get target directory from args or prompt user
|
|
||||||
target_str = args.target
|
target_str = args.target
|
||||||
if not target_str:
|
if not target_str:
|
||||||
target_str = input("Destination repo path (will be created if missing): ").strip()
|
target_str = input("Destination repo path (will be created if missing): ").strip()
|
||||||
|
|
@ -408,16 +299,15 @@ def main():
|
||||||
say("No target specified. Aborting.")
|
say("No target specified. Aborting.")
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
# Resolve and create target directory
|
|
||||||
target = Path(target_str).expanduser().resolve()
|
target = Path(target_str).expanduser().resolve()
|
||||||
target.mkdir(parents=True, exist_ok=True)
|
target.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
say(f"[=] Installing Cascading Development into: {target}")
|
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)
|
copy_install_assets_to_target(target)
|
||||||
|
|
||||||
# Step 2: Create standard folder structure
|
# Ensure folder layout
|
||||||
for p in [
|
for p in [
|
||||||
target / "Docs" / "features",
|
target / "Docs" / "features",
|
||||||
target / "Docs" / "discussions" / "reviews",
|
target / "Docs" / "discussions" / "reviews",
|
||||||
|
|
@ -429,28 +319,26 @@ def main():
|
||||||
]:
|
]:
|
||||||
p.mkdir(parents=True, exist_ok=True)
|
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)
|
seed_process_and_rules(target)
|
||||||
|
|
||||||
# Step 4: Initialize git & install pre-commit
|
# Initialize git & install pre-commit
|
||||||
ensure_git_repo(target)
|
ensure_git_repo(target)
|
||||||
install_precommit_hook(target)
|
install_precommit_hook(target)
|
||||||
|
|
||||||
# Step 5: Launch Ramble (if available and not disabled)
|
# Launch Ramble (if available)
|
||||||
req = None
|
req = None
|
||||||
if not args.no_ramble:
|
if not args.no_ramble:
|
||||||
req = run_ramble_and_collect(target, provider=args.provider, claude_cmd=args.claude_cmd)
|
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)
|
seed_initial_feature(target, req)
|
||||||
|
|
||||||
# Step 7: Perform initial commit
|
# First commit
|
||||||
first_commit(target)
|
first_commit(target)
|
||||||
|
|
||||||
# Completion message
|
|
||||||
say("[✓] Setup complete.")
|
say("[✓] Setup complete.")
|
||||||
say(f"Next steps:\n cd {target}\n git status")
|
say(f"Next steps:\n cd {target}\n git status")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -8,46 +8,149 @@ VER = (ROOT / "VERSION").read_text().strip() if (ROOT / "VERSION").exists() els
|
||||||
BUNDLE = OUT / f"cascadingdev-{VER}"
|
BUNDLE = OUT / f"cascadingdev-{VER}"
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# Removes the old install bundle if it already exists
|
|
||||||
if BUNDLE.exists():
|
if BUNDLE.exists():
|
||||||
shutil.rmtree(BUNDLE)
|
shutil.rmtree(BUNDLE)
|
||||||
# Create the directories
|
# copy essentials
|
||||||
(BUNDLE / "assets" / "hooks").mkdir(parents=True, exist_ok=True)
|
(BUNDLE / "assets" / "hooks").mkdir(parents=True, exist_ok=True)
|
||||||
(BUNDLE / "assets" / "templates").mkdir(parents=True, exist_ok=True)
|
(BUNDLE / "assets" / "templates").mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# 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" / "ramble.py", BUNDLE / "ramble.py")
|
||||||
shutil.copy2(ROOT / "assets" / "runtime" / "create_feature.py", BUNDLE / "create_feature.py")
|
|
||||||
|
|
||||||
shutil.copy2(ROOT / "assets" / "hooks" / "pre-commit", BUNDLE / "assets" / "hooks" / "pre-commit")
|
shutil.copy2(ROOT / "assets" / "hooks" / "pre-commit", BUNDLE / "assets" / "hooks" / "pre-commit")
|
||||||
|
for t in ["feature_request.md","discussion.md","design_doc.md"]:
|
||||||
# copy core templates
|
|
||||||
|
|
||||||
for t in [
|
|
||||||
"feature_request.md",
|
|
||||||
"feature.discussion.md",
|
|
||||||
"feature.discussion.sum.md",
|
|
||||||
"design_doc.md",
|
|
||||||
"USER_GUIDE.md",
|
|
||||||
"root_gitignore",
|
|
||||||
]:
|
|
||||||
shutil.copy2(ROOT / "assets" / "templates" / t, BUNDLE / "assets" / "templates" / t)
|
shutil.copy2(ROOT / "assets" / "templates" / t, BUNDLE / "assets" / "templates" / t)
|
||||||
|
|
||||||
# copy (recursively) the contents of process/ and rules/ templates folders
|
|
||||||
def copy_tree(src, dst):
|
|
||||||
if dst.exists():
|
|
||||||
shutil.rmtree(dst)
|
|
||||||
shutil.copytree(src, dst)
|
|
||||||
copy_tree(ROOT / "assets" / "templates" / "process", BUNDLE / "assets" / "templates" / "process")
|
|
||||||
copy_tree(ROOT / "assets" / "templates" / "rules", BUNDLE / "assets" / "templates" / "rules")
|
|
||||||
|
|
||||||
# write installer entrypoint
|
# write installer entrypoint
|
||||||
shutil.copy2(ROOT / "src" / "cascadingdev" / "setup_project.py",
|
(BUNDLE / "setup_cascadingdev.py").write_text(INSTALLER_PY, encoding="utf-8")
|
||||||
BUNDLE / "setup_cascadingdev.py")
|
|
||||||
|
|
||||||
(BUNDLE / "INSTALL.md").write_text("Unzip, then run:\n\n python3 setup_cascadingdev.py\n", encoding="utf-8")
|
(BUNDLE / "INSTALL.md").write_text("Unzip, then run:\n\n python3 setup_cascadingdev.py\n", encoding="utf-8")
|
||||||
(BUNDLE / "VERSION").write_text(VER, encoding="utf-8")
|
(BUNDLE / "VERSION").write_text(VER, encoding="utf-8")
|
||||||
print(f"[✓] Built installer → {BUNDLE}")
|
print(f"[✓] Built installer → {BUNDLE}")
|
||||||
|
|
||||||
|
INSTALLER_PY = r'''#!/usr/bin/env python3
|
||||||
|
import argparse, json, os, shutil, subprocess, sys, datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
HERE = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
def sh(cmd, cwd=None):
|
||||||
|
return subprocess.run(cmd, check=True, text=True, capture_output=True, cwd=cwd)
|
||||||
|
|
||||||
|
def say(x): print(x, flush=True)
|
||||||
|
|
||||||
|
def write_if_missing(p: Path, content: str):
|
||||||
|
p.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
if not p.exists():
|
||||||
|
p.write_text(content, encoding="utf-8")
|
||||||
|
|
||||||
|
def copytree(src: Path, dst: Path):
|
||||||
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
if src.is_file():
|
||||||
|
shutil.copy2(src, dst)
|
||||||
|
else:
|
||||||
|
shutil.copytree(src, dst, dirs_exist_ok=True)
|
||||||
|
|
||||||
|
def ensure_git_repo(target: Path):
|
||||||
|
if not (target / ".git").exists():
|
||||||
|
sh(["git", "init", "-b", "main"], cwd=target)
|
||||||
|
write_if_missing(target / ".gitignore", ".env\n.env.*\nsecrets/\n__pycache__/\n*.pyc\n.pytest_cache/\n.DS_Store\n")
|
||||||
|
|
||||||
|
def install_hook(target: Path):
|
||||||
|
hooks = target / ".git" / "hooks"; hooks.mkdir(parents=True, exist_ok=True)
|
||||||
|
src = HERE / "assets" / "hooks" / "pre-commit"
|
||||||
|
dst = hooks / "pre-commit"
|
||||||
|
dst.write_text(src.read_text(encoding="utf-8"), encoding="utf-8")
|
||||||
|
dst.chmod(0o755)
|
||||||
|
|
||||||
|
def run_ramble(target: Path, provider="mock"):
|
||||||
|
ramble = target / "ramble.py"
|
||||||
|
if not ramble.exists(): return None
|
||||||
|
args = [sys.executable, str(ramble),
|
||||||
|
"--provider", provider,
|
||||||
|
"--prompt", "Describe your initial feature request for this repository",
|
||||||
|
"--fields", "Summary", "Title", "Intent", "ProblemItSolves", "BriefOverview",
|
||||||
|
"--criteria", '{"Summary":"<= 2 sentences","Title":"camelCase, <= 24 chars"}']
|
||||||
|
say("[•] Launching Ramble…")
|
||||||
|
p = subprocess.run(args, text=True, capture_output=True, cwd=target)
|
||||||
|
try:
|
||||||
|
return json.loads((p.stdout or "").strip())
|
||||||
|
except Exception:
|
||||||
|
say("[-] Could not parse Ramble output; using template defaults.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def seed_rules_and_templates(target: Path):
|
||||||
|
write_if_missing(target / ".ai-rules.yml",
|
||||||
|
"version: 1\nfile_associations:\n \"*.md\": \"md-file\"\n\nrules:\n md-file:\n description: \"Normalize Markdown\"\n instruction: |\n Keep markdown tidy (headings, lists, spacing). No content churn.\nsettings:\n model: \"local-mock\"\n temperature: 0.1\n")
|
||||||
|
# copy templates
|
||||||
|
copytree(HERE / "assets" / "templates", target / "process" / "templates")
|
||||||
|
|
||||||
|
def seed_first_feature(target: Path, req):
|
||||||
|
today = datetime.date.today().isoformat()
|
||||||
|
fr = target / "Docs" / "features" / f"FR_{today}_initial-feature-request"
|
||||||
|
disc = fr / "discussions"; disc.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if req:
|
||||||
|
fields = req.get("fields", {}) or {}
|
||||||
|
title = (fields.get("Title") or "initialProjectDesign").strip()
|
||||||
|
intent = (fields.get("Intent") or "—").strip()
|
||||||
|
problem = (fields.get("ProblemItSolves") or "—").strip()
|
||||||
|
brief = (fields.get("BriefOverview") or "—").strip()
|
||||||
|
summary = (req.get("summary") or "").strip()
|
||||||
|
body = f"# Feature Request: {title}\n\n**Intent**: {intent}\n**Motivation / Problem**: {problem}\n**Brief Overview**: {brief}\n\n**Summary**: {summary}\n**Meta**: Created: {today}\n"
|
||||||
|
else:
|
||||||
|
body = (target / "process" / "templates" / "feature_request.md").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
(fr / "request.md").write_text(body, encoding="utf-8")
|
||||||
|
(disc / "feature.discussion.md").write_text(
|
||||||
|
f"---\ntype: discussion\nstage: feature\nstatus: OPEN\nfeature_id: FR_{today}_initial-feature-request\ncreated: {today}\n---\n## Summary\nKickoff discussion. Append comments below.\n\n## Participation\n- Maintainer: Kickoff. VOTE: READY\n", encoding="utf-8")
|
||||||
|
(disc / "feature.discussion.sum.md").write_text(
|
||||||
|
"# Summary — Feature\n\n<!-- SUMMARY:DECISIONS START -->\n## Decisions (ADR-style)\n- (none yet)\n<!-- SUMMARY:DECISIONS END -->\n\n<!-- SUMMARY:OPEN_QUESTIONS START -->\n## Open Questions\n- (none yet)\n<!-- SUMMARY:OPEN_QUESTIONS END -->\n\n<!-- SUMMARY:AWAITING START -->\n## Awaiting Replies\n- (none yet)\n<!-- SUMMARY:AWAITING END -->\n\n<!-- SUMMARY:ACTION_ITEMS START -->\n## Action Items\n- (none yet)\n<!-- SUMMARY:ACTION_ITEMS END -->\n\n<!-- SUMMARY:VOTES START -->\n## Votes (latest per participant)\nREADY: 1 • CHANGES: 0 • REJECT: 0\n- Maintainer\n<!-- SUMMARY:VOTES END -->\n\n<!-- SUMMARY:TIMELINE START -->\n## Timeline (most recent first)\n- {today} Maintainer: Kickoff\n<!-- SUMMARY:TIMELINE END -->\n\n<!-- SUMMARY:LINKS START -->\n## Links\n- Design/Plan: ../design/design.md\n<!-- SUMMARY:LINKS END -->\n".replace("{today}", today), encoding="utf-8")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--target", help="Destination path for the user's project")
|
||||||
|
ap.add_argument("--no-ramble", action="store_true")
|
||||||
|
ap.add_argument("--provider", default="mock")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
target = Path(args.target or input("User's project folder: ").strip()).expanduser().resolve()
|
||||||
|
target.mkdir(parents=True, exist_ok=True)
|
||||||
|
say(f"[=] Installing into: {target}")
|
||||||
|
|
||||||
|
# copy top-level assets
|
||||||
|
shutil.copy2(HERE / "DESIGN.md", target / "DESIGN.md")
|
||||||
|
shutil.copy2(HERE / "ramble.py", target / "ramble.py")
|
||||||
|
|
||||||
|
# basic tree
|
||||||
|
for p in [target / "Docs" / "features",
|
||||||
|
target / "Docs" / "discussions" / "reviews",
|
||||||
|
target / "Docs" / "diagrams" / "file_diagrams",
|
||||||
|
target / "scripts" / "hooks",
|
||||||
|
target / "src", target / "tests", target / "process"]:
|
||||||
|
p.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# rules / templates
|
||||||
|
seed_rules_and_templates(target)
|
||||||
|
|
||||||
|
# git + hook
|
||||||
|
ensure_git_repo(target)
|
||||||
|
install_hook(target)
|
||||||
|
|
||||||
|
# ramble
|
||||||
|
req = None if args.no_ramble else run_ramble(target, provider=args.provider)
|
||||||
|
|
||||||
|
# seed FR
|
||||||
|
seed_first_feature(target, req)
|
||||||
|
|
||||||
|
try:
|
||||||
|
sh(["git", "add", "-A"], cwd=target)
|
||||||
|
sh(["git", "commit", "-m", "chore: bootstrap Cascading Development scaffolding"], cwd=target)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
say("[✓] Done. Next:\n cd " + str(target) + "\n git status")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
'''
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
|
||||||
|
|
@ -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())
|
|
||||||
|
|
@ -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()
|
|
||||||
Loading…
Reference in New Issue