Compare commits

...

No commits in common. "main-simplified" and "pages" have entirely different histories.

48 changed files with 133 additions and 5709 deletions

5
.gitignore vendored
View File

@ -1,5 +0,0 @@
.venv/
.idea/
__pycache__/
*.pyc
install/

16
404.html Normal file

File diff suppressed because one or more lines are too long

101
CLAUDE.md
View File

@ -1,101 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Overview
**CascadingDev (CDev) - Simplified** is the core of a Git-native AI-human collaboration framework. This simplified version focuses on:
- Git pre-commit hooks with safety checks
- Cascading `.ai-rules.yml` system
- Ramble GUI for structured feature requests
- Installer bundle generation
For advanced discussion orchestration, see [Orchestrated Discussions](https://gitea.brrd.tech/rob/orchestrated-discussions).
### Key Concept: Two Repositories
- **CascadingDev repo** (this codebase): The tooling that builds installer bundles
- **User's project repo**: A new repository scaffolded by running the installer bundle
## Repository Architecture
### Directory Structure
```
CascadingDev/
├── src/cascadingdev/ # Core Python modules and CLI
│ ├── cli.py # Main CLI entry point (cdev command)
│ ├── setup_project.py # Installer script (copied to bundle)
│ └── utils.py # Shared utilities
├── assets/ # Single source of truth for shipped files
│ ├── hooks/pre-commit # Git hook template (bash script)
│ ├── templates/ # Markdown templates copied to user projects
│ │ ├── rules/ # .ai-rules.yml files
│ │ └── process/ # policies.yml
│ └── runtime/ # Python scripts copied to user projects
│ ├── ramble.py # GUI for feature creation (PySide6/PyQt5)
│ └── create_feature.py # CLI for feature creation
├── tools/ # Build and test scripts
│ ├── build_installer.py # Creates install/ bundle
│ └── smoke_test.py # Basic validation
├── install/ # Build output (git-ignored)
└── VERSION # Semantic version
```
## Common Commands
```bash
# Initial setup
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip wheel PySide6
# Install in development mode
pip install -e .
# Build the installer bundle
cdev build
# Test-install into a temporary folder
python install/cascadingdev-*/setup_cascadingdev.py --target /tmp/myproject --no-ramble
```
## Key Concepts
### Cascading Rules System
The `.ai-rules.yml` files define automation behavior. User projects have:
- Root `.ai-rules.yml` - Global defaults
- `Docs/features/.ai-rules.yml` - Feature-specific rules
Rules are hierarchical: nearest file takes precedence.
### Pre-commit Hook
The bash pre-commit hook (`assets/hooks/pre-commit`) provides:
- Scans for potential secrets (blocks commit on match)
- Ensures discussion files have companion `.sum.md` summary files
- Uses flock to prevent git corruption from concurrent commits
- Fast and lightweight (pure bash, no Python dependencies)
Environment variables:
- `CDEV_SKIP_HOOK=1` - Skip all hook checks
- `CDEV_SKIP_SUMMARIES=1` - Skip summary file generation
### Build System
The build process (`tools/build_installer.py`) creates a standalone installer bundle:
1. Reads version from `VERSION` file
2. Creates `install/cascadingdev-<version>/` directory
3. Copies essential files from `assets/` to bundle
4. Copies `src/cascadingdev/setup_project.py` as the installer entry point
## Related Projects
This project is part of a stack:
1. **[SmartTools](https://gitea.brrd.tech/rob/SmartTools)** - AI provider abstraction
2. **[Orchestrated Discussions](https://gitea.brrd.tech/rob/orchestrated-discussions)** - Multi-agent discussion orchestration
3. **[Ramble](https://gitea.brrd.tech/rob/ramble)** - AI-powered structured field extraction GUI
4. **[Artifact Editor](https://gitea.brrd.tech/rob/artifact-editor)** - AI-enhanced diagram and model creation

View File

@ -1,67 +0,0 @@
# CascadingDev Simplified - Git-native AI collaboration framework
#
# Build: docker build -t cascadingdev .
# Test: docker run -it --rm cascadingdev cdev --help
FROM python:3.12-slim
LABEL maintainer="rob"
LABEL description="CascadingDev - Git hooks and cascading rules framework"
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
# Qt6 dependencies for Ramble GUI (optional)
libgl1 \
libegl1 \
libxkbcommon0 \
libdbus-1-3 \
libxcb-cursor0 \
libxcb-icccm4 \
libxcb-keysyms1 \
libxcb-shape0 \
libxcb-xinerama0 \
libxcb-randr0 \
libxcb-render-util0 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copy project files
COPY pyproject.toml VERSION README.md ./
COPY src/ ./src/
COPY assets/ ./assets/
COPY tools/ ./tools/
COPY docs/ ./docs/
# Install CascadingDev
RUN pip install --no-cache-dir -e .
# Install PySide6 for Ramble GUI (optional, makes image larger)
RUN pip install --no-cache-dir PySide6 || true
# Verify installation
RUN cdev --help
# Default: show help
CMD ["cdev", "--help"]
# ==============================================================================
# Usage Examples
# ==============================================================================
# Build:
# docker build -t cascadingdev .
#
# Show help:
# docker run -it --rm cascadingdev
#
# Build installer bundle:
# docker run -it --rm -v $(pwd)/output:/output cascadingdev \
# bash -c "cdev build && cp -r install/* /output/"
#
# Install into a new project (headless):
# docker run -it --rm -v /path/to/project:/project cascadingdev \
# python install/cascadingdev-*/setup_cascadingdev.py --target /project --no-ramble
#
# Interactive shell:
# docker run -it --rm cascadingdev bash

View File

@ -1,76 +1 @@
# CascadingDev (CDev) - Simplified
**CDev** — short for *Cascading Development* — is a **Git-native AIhuman collaboration framework** that uses git hooks and cascading rules to enhance your development workflow.
This is the **simplified version** focused on core functionality:
- Git pre-commit hooks with safety checks
- Cascading `.ai-rules.yml` system
- Ramble GUI for capturing structured feature requests
For advanced discussion orchestration features, see [Orchestrated Discussions](https://gitea.brrd.tech/rob/orchestrated-discussions).
---
## Key Features
- **Cascading Rules System** — nearest `.ai-rules.yml` defines behavior at each directory level
- **Pre-commit Hook** — secret scanning, discussion summary creation, git corruption prevention
- **Ramble GUI** — PySide6/PyQt5 dialog for capturing structured feature requests
- **Deterministic Builds** — reproducible installer bundle
---
## Quick Start
```bash
# 1. Create and activate a virtual environment
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip wheel PySide6
# 2. Build the installer bundle
python tools/build_installer.py
# 3. Install into a project folder
python install/cascadingdev-*/setup_cascadingdev.py --target /path/to/myproject
```
## Project Structure
```
CascadingDev/
├── assets/
│ ├── hooks/pre-commit # Git pre-commit hook
│ ├── runtime/ # Runtime scripts (ramble.py, create_feature.py)
│ └── templates/ # Discussion and rule templates
├── src/cascadingdev/ # Python package
│ ├── setup_project.py # Project initialization
│ ├── cli.py # Command-line interface
│ └── ...
├── tools/ # Build and test tools
└── docs/ # Documentation
```
## Pre-commit Hook Features
The pre-commit hook provides:
1. **Secret Scanning** - Prevents accidental commit of API keys and secrets
2. **Summary Files** - Auto-creates `.sum.md` companion files for discussions
3. **Concurrency Safety** - Uses flock to prevent git corruption from parallel commits
Environment variables:
- `CDEV_SKIP_HOOK=1` - Skip all hook checks
- `CDEV_SKIP_SUMMARIES=1` - Skip summary file generation
## Related Projects
This project is part of a three-layer stack:
1. **[SmartTools](https://gitea.brrd.tech/rob/SmartTools)** - AI provider abstraction and tool execution
2. **[Orchestrated Discussions](https://gitea.brrd.tech/rob/orchestrated-discussions)** - Multi-agent discussion orchestration
3. **[Ramble](https://gitea.brrd.tech/rob/ramble)** - AI-powered structured field extraction GUI
## License
MIT
# CascadingDev (Simplified) Documentation

View File

@ -1 +0,0 @@
0.1.0

File diff suppressed because one or more lines are too long

View File

@ -1,124 +0,0 @@
#!/usr/bin/env bash
#
# CascadingDev Pre-commit Hook
# =============================
# This hook provides safety checks during git commits.
#
# What it does:
# 1. Scans for potential secrets in staged changes
# 2. Creates companion summary files (.sum.md) for discussion files
#
# Environment Variables:
# CDEV_SKIP_HOOK=1 Skip all checks (hook exits immediately)
# CDEV_SKIP_SUMMARIES=1 Skip summary file generation
#
# Safety: Exits on errors to prevent broken commits
set -euo pipefail
if [[ -n "${CDEV_SKIP_HOOK:-}" ]]; then
exit 0
fi
# Navigate to git repository root so all file paths work correctly
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo ".")"
cd "$ROOT"
# ============================================================================
# CRITICAL: Acquire Hook Execution Lock
# ============================================================================
# Prevents concurrent hook executions from corrupting Git repository.
# Race condition scenario:
# - Process A runs `git add file1.md`, computes blob SHA, starts writing to .git/objects/
# - Process B runs `git add file2.md` concurrently
# - Blob object creation fails, leaving orphaned SHA in index
# - Result: "error: invalid object 100644 <SHA> for '<file>'"
#
# Solution: Use flock to ensure only one hook instance runs at a time.
# The lock is automatically released when this script exits.
# ============================================================================
LOCK_FILE="${ROOT}/.git/hooks/pre-commit.lock"
exec 9>"$LOCK_FILE"
if ! flock -n 9; then
echo >&2 "[pre-commit] Another pre-commit hook is running. Waiting for lock..."
flock 9 # Block until lock is available
echo >&2 "[pre-commit] Lock acquired, continuing..."
fi
# Cleanup: Remove lock file on exit
trap 'rm -f "$LOCK_FILE"' EXIT
# -------- collect staged files ----------
mapfile -t STAGED < <(git diff --cached --name-only --diff-filter=AM || true)
[ "${#STAGED[@]}" -eq 0 ] && exit 0
# -------- tiny secret scan (fast, regex only) ----------
# Abort commit if staged changes contain potential secrets matching common patterns
DIFF="$(git diff --cached)"
if echo "$DIFF" | grep -Eqi '(api[_-]?key|secret|access[_-]?token|private[_-]?key)[:=]\s*[A-Za-z0-9_\-]{12,}'; then
echo >&2 "[pre-commit] Possible secret detected in staged changes."
echo >&2 " If false positive, commit with --no-verify and add an allowlist later."
exit 11
fi
# -------- ensure discussion summaries exist (companion files) ----------
if [[ -z "${CDEV_SKIP_SUMMARIES:-}" ]]; then
ensure_summary() {
local disc="$1"
local dir; dir="$(dirname "$disc")"
local sum="$dir/$(basename "$disc" .md).sum.md"
if [ ! -f "$sum" ]; then
cat > "$sum" <<'EOF'
# Summary — <Stage Title>
<!-- 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: 0 • CHANGES: 0 • REJECT: 0
- (no votes yet)
<!-- SUMMARY:VOTES END -->
<!-- SUMMARY:TIMELINE START -->
## Timeline (most recent first)
- <YYYY-MM-DD HH:MM> <name>: <one-liner>
<!-- SUMMARY:TIMELINE END -->
<!-- SUMMARY:LINKS START -->
## Links
- Related PRs:
- Commits:
- Design/Plan: ../design/design.md
<!-- SUMMARY:LINKS END -->
EOF
git add "$sum"
fi
}
# Process each staged discussion file and ensure it has a summary
for f in "${STAGED[@]}"; do
case "$f" in
Docs/features/*/discussions/*.discussion.md) ensure_summary "$f";;
esac
done
fi
exit 0

View File

@ -0,0 +1 @@
"use strict";(globalThis.webpackChunkproject_public_docs=globalThis.webpackChunkproject_public_docs||[]).push([[555],{8021(e){e.exports=JSON.parse('{"version":{"pluginId":"default","version":"current","label":"Next","banner":null,"badge":false,"noIndex":false,"className":"docs-version-current","isLast":true,"docsSidebars":{"docs":[{"type":"link","href":"/rob/CascadingDev_simplified/","label":"CascadingDev (Simplified)","docId":"overview","unlisted":false}]},"docs":{"overview":{"id":"overview","title":"CascadingDev (Simplified)","description":"CascadingDev (CDev) is a Git-native AI-human collaboration framework that uses git hooks and cascading rules to enhance development workflows. This simplified version focuses on core functionality: pre-commit safety hooks, cascading .ai-rules.yml configuration, and the Ramble GUI for structured feature requests.","sidebar":"docs"}}}}')}}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
"use strict";(globalThis.webpackChunkproject_public_docs=globalThis.webpackChunkproject_public_docs||[]).push([[237],{2237(e,t,i){i.r(t),i.d(t,{default:()=>l});i(6540);var o=i(1312),n=i(5500),s=i(1656),r=i(3363),a=i(4848);function l(){const e=(0,o.T)({id:"theme.NotFound.title",message:"Page Not Found"});return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(n.be,{title:e}),(0,a.jsx)(s.A,{children:(0,a.jsx)(r.A,{})})]})}},3363(e,t,i){i.d(t,{A:()=>a});i(6540);var o=i(4164),n=i(1312),s=i(1107),r=i(4848);function a({className:e}){return(0,r.jsx)("main",{className:(0,o.A)("container margin-vert--xl",e),children:(0,r.jsx)("div",{className:"row",children:(0,r.jsxs)("div",{className:"col col--6 col--offset-3",children:[(0,r.jsx)(s.A,{as:"h1",className:"hero__title",children:(0,r.jsx)(n.A,{id:"theme.NotFound.title",description:"The title of the 404 page",children:"Page Not Found"})}),(0,r.jsx)("p",{children:(0,r.jsx)(n.A,{id:"theme.NotFound.p1",description:"The first paragraph of the 404 page",children:"We could not find what you were looking for."})}),(0,r.jsx)("p",{children:(0,r.jsx)(n.A,{id:"theme.NotFound.p2",description:"The 2nd paragraph of the 404 page",children:"Please contact the owner of the site that linked you to the original URL and let them know their link is broken."})})]})})})}}}]);

View File

@ -0,0 +1 @@
"use strict";(globalThis.webpackChunkproject_public_docs=globalThis.webpackChunkproject_public_docs||[]).push([[647],{7121(e,c,s){s.r(c),s.d(c,{default:()=>t});s(6540);var r=s(4164),u=s(7559),a=s(5500),l=s(2831),o=s(1656),p=s(4848);function t(e){return(0,p.jsx)(a.e3,{className:(0,r.A)(u.G.wrapper.docsPages),children:(0,p.jsx)(o.A,{children:(0,l.v)(e.route.routes)})})}}}]);

View File

@ -0,0 +1 @@
"use strict";(globalThis.webpackChunkproject_public_docs=globalThis.webpackChunkproject_public_docs||[]).push([[98],{1723(n,e,s){s.r(e),s.d(e,{default:()=>d});s(6540);var r=s(5500);function o(n,e){return`docs-${n}-${e}`}var c=s(3025),t=s(2831),i=s(1463),u=s(4848);function l(n){const{version:e}=n;return(0,u.jsxs)(u.Fragment,{children:[(0,u.jsx)(i.A,{version:e.version,tag:o(e.pluginId,e.version)}),(0,u.jsx)(r.be,{children:e.noIndex&&(0,u.jsx)("meta",{name:"robots",content:"noindex, nofollow"})})]})}function a(n){const{version:e,route:s}=n;return(0,u.jsx)(r.e3,{className:e.className,children:(0,u.jsx)(c.n,{version:e,children:(0,t.v)(s.routes)})})}function d(n){return(0,u.jsxs)(u.Fragment,{children:[(0,u.jsx)(l,{...n}),(0,u.jsx)(a,{...n})]})}}}]);

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
"use strict";(globalThis.webpackChunkproject_public_docs=globalThis.webpackChunkproject_public_docs||[]).push([[742],{7093(c){c.exports=JSON.parse('{"name":"docusaurus-plugin-content-docs","id":"default"}')}}]);

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,61 @@
/* NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress
* @license MIT */
/**
* @license React
* react-dom-client.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-dom.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-jsx-runtime.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v16.13.1
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

View File

@ -0,0 +1 @@
(()=>{"use strict";var e,r,t,a,o,n={},i={};function l(e){var r=i[e];if(void 0!==r)return r.exports;var t=i[e]={id:e,loaded:!1,exports:{}};return n[e].call(t.exports,t,t.exports,l),t.loaded=!0,t.exports}l.m=n,l.c=i,e=[],l.O=(r,t,a,o)=>{if(!t){var n=1/0;for(u=0;u<e.length;u++){for(var[t,a,o]=e[u],i=!0,c=0;c<t.length;c++)(!1&o||n>=o)&&Object.keys(l.O).every(e=>l.O[e](t[c]))?t.splice(c--,1):(i=!1,o<n&&(n=o));if(i){e.splice(u--,1);var d=a();void 0!==d&&(r=d)}}return r}o=o||0;for(var u=e.length;u>0&&e[u-1][2]>o;u--)e[u]=e[u-1];e[u]=[t,a,o]},l.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return l.d(r,{a:r}),r},t=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,l.t=function(e,a){if(1&a&&(e=this(e)),8&a)return e;if("object"==typeof e&&e){if(4&a&&e.__esModule)return e;if(16&a&&"function"==typeof e.then)return e}var o=Object.create(null);l.r(o);var n={};r=r||[null,t({}),t([]),t(t)];for(var i=2&a&&e;("object"==typeof i||"function"==typeof i)&&!~r.indexOf(i);i=t(i))Object.getOwnPropertyNames(i).forEach(r=>n[r]=()=>e[r]);return n.default=()=>e,l.d(o,n),o},l.d=(e,r)=>{for(var t in r)l.o(r,t)&&!l.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},l.f={},l.e=e=>Promise.all(Object.keys(l.f).reduce((r,t)=>(l.f[t](e,r),r),[])),l.u=e=>"assets/js/"+({48:"a94703ab",98:"a7bd4aaa",401:"17896441",413:"1db64337",555:"16300c58",647:"5e95c892",742:"aba21aa0"}[e]||e)+"."+{48:"b8c77466",98:"3ba34601",237:"447ba118",401:"a2525508",413:"c231d1e2",555:"849ca54a",647:"a3b66919",742:"4a552a5c"}[e]+".js",l.miniCssF=e=>{},l.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),a={},o="project-public-docs:",l.l=(e,r,t,n)=>{if(a[e])a[e].push(r);else{var i,c;if(void 0!==t)for(var d=document.getElementsByTagName("script"),u=0;u<d.length;u++){var s=d[u];if(s.getAttribute("src")==e||s.getAttribute("data-webpack")==o+t){i=s;break}}i||(c=!0,(i=document.createElement("script")).charset="utf-8",l.nc&&i.setAttribute("nonce",l.nc),i.setAttribute("data-webpack",o+t),i.src=e),a[e]=[r];var f=(r,t)=>{i.onerror=i.onload=null,clearTimeout(p);var o=a[e];if(delete a[e],i.parentNode&&i.parentNode.removeChild(i),o&&o.forEach(e=>e(t)),r)return r(t)},p=setTimeout(f.bind(null,void 0,{type:"timeout",target:i}),12e4);i.onerror=f.bind(null,i.onerror),i.onload=f.bind(null,i.onload),c&&document.head.appendChild(i)}},l.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.p="/rob/CascadingDev_simplified/",l.gca=function(e){return e={17896441:"401",a94703ab:"48",a7bd4aaa:"98","1db64337":"413","16300c58":"555","5e95c892":"647",aba21aa0:"742"}[e]||e,l.p+l.u(e)},(()=>{var e={354:0,869:0};l.f.j=(r,t)=>{var a=l.o(e,r)?e[r]:void 0;if(0!==a)if(a)t.push(a[2]);else if(/^(354|869)$/.test(r))e[r]=0;else{var o=new Promise((t,o)=>a=e[r]=[t,o]);t.push(a[2]=o);var n=l.p+l.u(r),i=new Error;l.l(n,t=>{if(l.o(e,r)&&(0!==(a=e[r])&&(e[r]=void 0),a)){var o=t&&("load"===t.type?"missing":t.type),n=t&&t.target&&t.target.src;i.message="Loading chunk "+r+" failed.\n("+o+": "+n+")",i.name="ChunkLoadError",i.type=o,i.request=n,a[1](i)}},"chunk-"+r,r)}},l.O.j=r=>0===e[r];var r=(r,t)=>{var a,o,[n,i,c]=t,d=0;if(n.some(r=>0!==e[r])){for(a in i)l.o(i,a)&&(l.m[a]=i[a]);if(c)var u=c(l)}for(r&&r(t);d<n.length;d++)o=n[d],l.o(e,o)&&e[o]&&e[o][0](),e[o]=0;return l.O(u)},t=globalThis.webpackChunkproject_public_docs=globalThis.webpackChunkproject_public_docs||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})()})();

View File

@ -1,262 +0,0 @@
#!/usr/bin/env python3
"""
create_feature.py create a new feature request (+ discussion & summary)
Usage:
python create_feature.py --title "My Idea"
python create_feature.py --no-ramble
python create_feature.py --dir /path/to/repo
Behavior:
- Prefer Ramble (ramble.py in repo root) unless --no-ramble is passed.
- If Ramble not present or fails, prompt for fields in terminal.
- Fields come from the feature_request.md template when possible.
"""
from __future__ import annotations
import argparse, datetime, json, os, re, subprocess, sys
from pathlib import Path
from typing import Dict, List, Tuple
# --------- helpers ---------
def say(msg: str) -> None:
print(msg, flush=True)
def git_root_or_cwd(start: Path) -> Path:
try:
cp = subprocess.run(["git", "rev-parse", "--show-toplevel"],
text=True, capture_output=True, check=True, cwd=start)
return Path(cp.stdout.strip())
except Exception:
return start
def read_text(p: Path) -> str:
return p.read_text(encoding="utf-8") if p.exists() else ""
def write_text(p: Path, s: str) -> None:
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(s, encoding="utf-8")
def slugify(s: str) -> str:
s = s.strip().lower()
s = re.sub(r"[^a-z0-9]+", "-", s)
s = re.sub(r"-{2,}", "-", s).strip("-")
return s or "feature"
def today() -> str:
return datetime.date.today().isoformat()
def find_template_fields(tmpl: str) -> List[Tuple[str, str]]:
"""
Scan template for lines like:
**Intent**: <...>
Return list of (FieldName, placeholderText).
"""
fields = []
for m in re.finditer(r"^\s*\*\*(.+?)\*\*:\s*(<[^>]+>|.*)$", tmpl, flags=re.M):
label = m.group(1).strip()
placeholder = m.group(2).strip()
# skip meta/system fields the script will generate
if label.lower().startswith("feature id") or label.lower().startswith("meta"):
continue
fields.append((label, placeholder))
return fields
def default_fields() -> List[str]:
return ["Title", "Intent", "Motivation / Problem", "Constraints / Non-Goals",
"Rough Proposal", "Open Questions", "Author"]
def collect_via_prompts(field_labels: List[str]) -> Dict[str, str]:
say("[•] Ramble disabled or not found; collecting fields in terminal…")
out = {}
for label in field_labels:
try:
val = input(f"{label}: ").strip()
except EOFError:
val = ""
out[label] = val
if "Title" not in out or not out["Title"].strip():
out["Title"] = "initialProjectDesign"
return out
def try_ramble(repo_root: Path, field_labels: List[str], provider: str, claude_cmd: str) -> Dict[str, str] | None:
ramble = repo_root / "ramble.py"
if not ramble.exists():
return None
args = [sys.executable, str(ramble),
"--provider", provider,
"--claude-cmd", claude_cmd,
"--prompt", "Describe your feature idea in your own words",
"--fields"] + field_labels + [
"--criteria", json.dumps({
"Title": "camelCase or kebab-case, <= 32 chars",
"Intent": "<= 2 sentences"
})
]
say("[•] Launching Ramble… (submit to return)")
cp = subprocess.run(args, text=True, capture_output=True, cwd=repo_root)
if cp.stderr and cp.stderr.strip():
say("[ramble stderr]\n" + cp.stderr.strip())
try:
data = json.loads((cp.stdout or "").strip())
except Exception:
return None
# Normalize: accept either {"fields":{...}} or flat {"Title":...}
fields = data.get("fields") if isinstance(data, dict) else None
if not isinstance(fields, dict):
fields = {k: data.get(k, "") for k in field_labels}
return {k: (fields.get(k) or "").strip() for k in field_labels}
def render_request_from_template(tmpl: str, fields: Dict[str, str], fid: str, created: str) -> str:
# if template has <title>, replace; also replace known placeholders if present
body = tmpl
replacements = {
"<title>": fields.get("Title", ""),
"<one paragraph describing purpose>": fields.get("Intent", ""),
"<why this is needed now>": fields.get("Motivation / Problem", ""),
"<bulleted list of limitations>": fields.get("Constraints / Non-Goals", ""),
"<short implementation outline>": fields.get("Rough Proposal", ""),
"<bulleted list of uncertainties>": fields.get("Open Questions", ""),
"<name>": fields.get("Author", ""),
}
for needle, val in replacements.items():
body = body.replace(needle, val)
# Append meta block if not already present
if "Feature ID" not in body or "Meta" not in body:
meta = f"""
**Feature ID**: {fid}
**Meta**: Created: {created} Author: {fields.get('Author','').strip() or ''}
""".lstrip()
body = body.strip() + "\n\n" + meta
return body.strip() + "\n"
def seed_discussion_files(dir_disc: Path, fid: str, created: str) -> None:
req = f"""---
type: discussion
stage: feature
status: OPEN
feature_id: {fid}
created: {created}
---
## Summary
Initial discussion for feature `{fid}`. Append your comments below.
## Participation
- Maintainer: Kickoff. VOTE: READY
"""
write_text(dir_disc / "feature.feature.discussion.md", req)
sum_md = f"""# Summary — Feature
<!-- SUMMARY:DECISIONS START -->
## Decisions (ADR-style)
- (none yet)
<!-- SUMMARY:DECISIONS END -->
<!-- SUMMARY:OPEN_QUESTIONS START -->
## Open Questions
- (none yet)
<!-- SUMMARY:OPEN_QUESTIONS END -->
<!-- SUMMARY:AWAITING START -->
## Awaiting Replies
- (none yet)
<!-- SUMMARY:AWAITING END -->
<!-- SUMMARY:ACTION_ITEMS START -->
## Action Items
- (none yet)
<!-- SUMMARY:ACTION_ITEMS END -->
<!-- SUMMARY:VOTES START -->
## Votes (latest per participant)
READY: 1 CHANGES: 0 REJECT: 0
- Maintainer
<!-- SUMMARY:VOTES END -->
<!-- SUMMARY:TIMELINE START -->
## Timeline (most recent first)
- {created} Maintainer: Kickoff
<!-- SUMMARY:TIMELINE END -->
<!-- SUMMARY:LINKS START -->
## Links
- Design/Plan: ../design/design.md
<!-- SUMMARY:LINKS END -->
"""
write_text(dir_disc / "feature.discussion.sum.md", sum_md)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--dir", help="Repo root (defaults to git root or CWD)")
ap.add_argument("--title", help="Feature title (useful without Ramble)")
ap.add_argument("--no-ramble", action="store_true", help="Disable Ramble UI")
ap.add_argument("--provider", choices=["mock", "claude"], default="mock")
ap.add_argument("--claude-cmd", default="claude")
args = ap.parse_args()
start = Path(args.dir).expanduser().resolve() if args.dir else Path.cwd()
repo = git_root_or_cwd(start)
say(f"[=] Using repository: {repo}")
tmpl_path = repo / "process" / "templates" / "feature_request.md"
tmpl = read_text(tmpl_path)
parsed_fields = find_template_fields(tmpl) or [(f, "") for f in default_fields()]
field_labels = [name for (name, _) in parsed_fields]
if "Title" not in field_labels:
field_labels = ["Title"] + field_labels
# Try Ramble unless disabled
fields: Dict[str, str] | None = None
if not args.no_ramble:
fields = try_ramble(repo, field_labels, provider=args.provider, claude_cmd=args.claude_cmd)
# Terminal prompts fallback
if not fields:
fields = collect_via_prompts(field_labels)
if args.title:
fields["Title"] = args.title
# Derive slug & feature id
slug = slugify(fields.get("Title", "") or args.title or "feature")
fid = f"FR_{today()}_{slug}"
# Build target paths
fr_dir = repo / "Docs" / "features" / fid
disc_dir = fr_dir / "discussions"
fr_dir.mkdir(parents=True, exist_ok=True)
disc_dir.mkdir(parents=True, exist_ok=True)
# Render request.md
if tmpl:
body = render_request_from_template(tmpl, fields, fid=fid, created=today())
else:
# fallback body
body = f"""# Feature Request: {fields.get('Title','')}
**Intent**: {fields.get('Intent','')}
**Motivation / Problem**: {fields.get('Motivation / Problem','')}
**Constraints / Non-Goals**:
{fields.get('Constraints / Non-Goals','')}
**Rough Proposal**:
{fields.get('Rough Proposal','')}
**Open Questions**:
{fields.get('Open Questions','')}
**Feature ID**: {fid}
**Meta**: Created: {today()} Author: {fields.get('Author','')}
"""
write_text(fr_dir / "request.md", body)
# Seed discussion & summary
seed_discussion_files(disc_dir, fid=fid, created=today())
say(f"[✓] Created feature at: {fr_dir}")
say("Next:")
say(f" git add {fr_dir}")
say(f" git commit -m \"feat: start {fid}\"")
if __name__ == "__main__":
main()

View File

@ -1,759 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
r"""
Ramble Generate (PlantUML + optional images, locked-field context, per-field criteria)
Whats new:
- Locked fields feed back into the prompt as authoritative context.
- Summary is just another field; all fields can have custom criteria.
- Fields are always editable; Lock checkbox prevents overwrite and highlights the field.
- How to use toggle with simple instructions.
- Soft colors; Ramble input visually stands out.
- Scrollable main content, Generate under ramble, Submit bottom-right returns JSON.
CLI examples
------------
# Mock provider (no external calls)
python3 ramble.py --fields Title Summary "Problem it solves" --criteria '{"Summary":"<=2 sentences","Title":"camelCase, no spaces, <=20 chars"}'
# Claude CLI
python3 ramble.py \
--provider=claude \
--claude-cmd=/home/rob/.npm-global/bin/claude \
--fields Title Summary "Problem it solves" "Brief overview" \
--criteria '{"Summary":"<= 2 sentences","Title":"camelCase, no spaces, <= 20 characters"}' \
--tail 6000 --debug
Requirements
------------
- PlantUML CLI in PATH -> sudo apt install plantuml
- PySide6 (or PyQt5) -> pip install PySide6
- requests (only for images) -> pip install requests
- Stability key (optional) -> STABILITY_API_KEY
- Pexels key (optional) -> PEXELS_API_KEY
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Dict, List, Optional, Any, Tuple, Protocol, runtime_checkable, Mapping, cast
import os, sys, json, textwrap, base64, re, time, shutil, subprocess, argparse, threading
try:
import requests # type: ignore
except ImportError:
requests = None
# ── Qt (PySide6 preferred; PyQt5 fallback) ────────────────────────────────────
QT_LIB = None
try:
from PySide6.QtCore import (Qt, QTimer, QPropertyAnimation, QEasingCurve, QObject,
QThreadPool, QRunnable, Signal, Slot)
from PySide6.QtGui import (QFont, QLinearGradient, QPainter, QPalette, QColor, QPixmap, QTextCursor)
from PySide6.QtWidgets import (
QApplication, QDialog, QGridLayout, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QPlainTextEdit, QTextEdit, QWidget, QGroupBox, QFormLayout, QSizePolicy,
QFrame, QMessageBox, QScrollArea, QCheckBox
)
QT_LIB = "PySide6"
except ImportError:
from PyQt5.QtCore import (Qt, QTimer, QPropertyAnimation, QEasingCurve, QObject,
QThreadPool, QRunnable, pyqtSignal as Signal, pyqtSlot as Slot)
from PyQt5.QtGui import (QFont, QLinearGradient, QPainter, QPalette, QColor, QPixmap, QTextCursor)
from PyQt5.QtWidgets import (
QApplication, QDialog, QGridLayout, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QPlainTextEdit, QTextEdit, QWidget, QGroupBox, QFormLayout, QSizePolicy,
QFrame, QMessageBox, QScrollArea, QCheckBox
)
QT_LIB = "PyQt5"
# ── Provider Protocol ─────────────────────────────────────────────────────────
@runtime_checkable
class RambleProvider(Protocol):
"""
generate() must return:
- summary: str
- fields: Dict[str, str]
- uml_blocks: List[Tuple[str, Optional[bytes]]]
- image_descriptions: List[str]
"""
def generate(
self,
*,
prompt: str,
ramble_text: str,
fields: List[str],
field_criteria: Dict[str, str],
locked_context: Dict[str, str],
) -> Dict[str, Any]:
...
# ── Mock Provider ─────────────────────────────────────────────────────────────
class MockProvider:
def generate(
self, *, prompt: str, ramble_text: str, fields: List[str],
field_criteria: Dict[str, str], locked_context: Dict[str, str]
) -> Dict[str, Any]:
words = ramble_text.strip().split()
cap = min(25, len(words))
summary = " ".join(words[-cap:]) if words else "(no content yet)"
summary = (summary[:1].upper() + summary[1:]).rstrip(".") + "."
field_map = {}
for f in fields:
crit = field_criteria.get(f, "").strip()
suffix = f" [criteria: {crit}]" if crit else ""
field_map[f] = f"{f}: Derived from ramble ({len(words)} words).{suffix}"
uml_blocks = [
("@startuml\nactor User\nUser -> System: Ramble\nSystem -> LLM: Generate\nLLM --> System: Summary/Fields/UML\nSystem --> User: Updates\n@enduml", None)
]
image_descriptions = [
"Illustrate the core actor interacting with the system.",
"Abstract icon that represents the ideas domain."
]
return {
"summary": summary,
"fields": field_map,
"uml_blocks": uml_blocks,
"image_descriptions": image_descriptions,
}
# ── Claude CLI Provider ───────────────────────────────────────────────────────
class ClaudeCLIProvider:
def __init__(
self,
cmd: str = "claude",
extra_args: Optional[List[str]] = None,
timeout_s: int = 120,
tail_chars: int = 8000,
use_arg_p: bool = True,
debug: bool = False,
log_path: str = "/tmp/ramble_claude.log",
):
self.cmd = shutil.which(cmd) or cmd
self.extra_args = extra_args or []
self.timeout_s = timeout_s
self.tail_chars = tail_chars
self.use_arg_p = use_arg_p
self.debug = debug
self.log_path = log_path
def _log(self, msg: str):
if not self.debug: return
with open(self.log_path, "a", encoding="utf-8") as f:
print(f"[{time.strftime('%H:%M:%S')}] {msg}", file=f)
def _build_prompt(
self, *, user_prompt: str, ramble_text: str,
fields: List[str], field_criteria: Dict[str,str], locked_context: Dict[str,str]
) -> str:
fields_yaml = "\n".join([f'- "{f}"' for f in fields])
criteria_yaml = "\n".join([f'- {name}: {field_criteria[name]}' for name in fields if field_criteria.get(name)])
locked_yaml = "\n".join([f'- {k}: {locked_context[k]}' for k in locked_context.keys()]) if locked_context else ""
ramble_tail = ramble_text[-self.tail_chars:] if self.tail_chars and len(ramble_text) > self.tail_chars else ramble_text
return textwrap.dedent(f"""\
You are an assistant that returns ONLY compact JSON. No preamble, no markdown, no code fences.
Goal:
- Read the user's "ramble" and synthesize structured outputs.
Required JSON keys:
- "summary": string
- "fields": object with these keys: {fields_yaml}
- "uml_blocks": array of objects: {{"uml_text": string, "png_base64": null}}
- "image_descriptions": array of short strings
Per-field criteria (enforce tightly; if ambiguous, choose the most useful interpretation):
{criteria_yaml if criteria_yaml else "- (no special criteria provided)"}
Guidance:
- The PlantUML diagram must reflect concrete entities/flows drawn from the ramble and any locked fields.
- Use 37 nodes where possible; prefer meaningful arrow labels.
- Image descriptions must be specific to this idea (avoid generic phrasing).
Authoritative context from previously LOCKED fields (treat as ground truth if present):
{locked_yaml if locked_yaml else "- (none locked yet)"}
Prompt: {user_prompt}
Ramble (tail, possibly truncated):
{ramble_tail}
""").strip() + "\n"
@staticmethod
def _strip_fences(s: str) -> str:
s = s.strip()
if s.startswith("```"):
s = re.sub(r"^```(?:json)?\s*", "", s, flags=re.IGNORECASE)
s = re.sub(r"\s*```$", "", s)
return s.strip()
def _run_once(self, prompt_text: str, timeout: int) -> str:
if self.use_arg_p:
argv = [self.cmd, "-p", prompt_text, *self.extra_args]
stdin = None
else:
argv = [self.cmd, *self.extra_args]
stdin = prompt_text
self._log(f"argv: {argv}")
t0 = time.time()
try:
proc = subprocess.run(argv, input=stdin, capture_output=True, text=True, timeout=timeout, check=False)
except FileNotFoundError as e:
self._log(f"FileNotFoundError: {e}")
raise RuntimeError(f"Claude CLI not found at {self.cmd}.") from e
except subprocess.TimeoutExpired:
self._log("TimeoutExpired"); raise
out = (proc.stdout or "").strip()
err = (proc.stderr or "").strip()
self._log(f"rc={proc.returncode} elapsed={time.time()-t0:.2f}s out={len(out)}B err={len(err)}B")
if proc.returncode != 0:
raise RuntimeError(f"Claude CLI exited {proc.returncode}:\n{err or out or '(no output)'}")
return out
def _normalize(self, raw: Dict[str, Any], fields: List[str]) -> Dict[str, Any]:
fields_map: Mapping[str, Any] = raw.get("fields", {}) or {}
uml_objs = raw.get("uml_blocks", []) or []
image_desc = raw.get("image_descriptions", []) or []
uml_blocks: List[tuple[str, Optional[bytes]]] = []
for obj in uml_objs:
uml_text = (obj or {}).get("uml_text") or ""
png_b64 = (obj or {}).get("png_base64")
png_bytes = None
if isinstance(png_b64, str) and png_b64:
try:
png_bytes = base64.b64decode(png_b64)
except Exception:
png_bytes = None
uml_blocks.append((uml_text, png_bytes))
normalized_fields = {name: str(fields_map.get(name, "")) for name in fields}
return {
"summary": str(raw.get("summary", "")),
"fields": normalized_fields,
"uml_blocks": uml_blocks,
"image_descriptions": [str(s) for s in image_desc],
}
def generate(
self, *, prompt: str, ramble_text: str, fields: List[str],
field_criteria: Dict[str, str], locked_context: Dict[str, str]
) -> Dict[str, Any]:
p = self._build_prompt(
user_prompt=prompt, ramble_text=ramble_text,
fields=fields, field_criteria=field_criteria, locked_context=locked_context
)
try:
out = self._run_once(p, timeout=self.timeout_s)
except subprocess.TimeoutExpired:
# Retry with a smaller tail but still honoring tail_chars if set
rt = ramble_text[-min(self.tail_chars or 3000, 3000):]
self._log("Retrying with smaller prompt…")
shorter = self._build_prompt(
user_prompt=prompt, ramble_text=rt,
fields=fields, field_criteria=field_criteria, locked_context=locked_context
)
out = self._run_once(shorter, timeout=max(45, self.timeout_s // 2))
txt = self._strip_fences(out)
try:
data = json.loads(txt)
except json.JSONDecodeError:
words = txt.split()
summary = " ".join(words[:30]) + ("..." if len(words) > 30 else "")
data = {
"summary": summary or "(no content)",
"fields": {f: f"{f}: {summary}" for f in fields},
"uml_blocks": [{"uml_text": "@startuml\nactor User\nUser -> System: Ramble\n@enduml", "png_base64": None}],
"image_descriptions": ["Abstract illustration related to the idea."],
}
return self._normalize(data, fields)
# ── UI helpers ─────────────────────────────────────────────────────────────────
class FadeLabel(QLabel):
def __init__(self, parent=None):
super().__init__(parent)
self._anim = QPropertyAnimation(self, b"windowOpacity")
self._anim.setDuration(300)
self._anim.setEasingCurve(QEasingCurve.InOutQuad)
self.setWindowOpacity(1.0)
def fade_to_text(self, text: str):
def set_text():
self.setText(text)
try: self._anim.finished.disconnect(set_text) # type: ignore
except Exception: pass
self._anim.setStartValue(0.0); self._anim.setEndValue(1.0); self._anim.start()
self._anim.stop()
self._anim.setStartValue(1.0); self._anim.setEndValue(0.0)
self._anim.finished.connect(set_text) # type: ignore
self._anim.start()
class FadingRambleEdit(QPlainTextEdit):
def __init__(self, max_blocks: int = 500, parent=None):
super().__init__(parent)
self.setPlaceholderText("Start here. Ramble about your idea…")
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setFrameStyle(QFrame.StyledPanel | QFrame.Raised)
self.document().setMaximumBlockCount(max_blocks)
font = QFont("DejaVu Sans Mono"); font.setPointSize(11)
self.setFont(font)
self.setFixedHeight(190)
# Soft highlight
self.setStyleSheet("""
QPlainTextEdit {
background:#f4fbff; border:2px solid #9ed6ff; border-radius:8px; padding:8px;
}
""")
def wheelEvent(self, event): event.ignore()
def keyPressEvent(self, event):
super().keyPressEvent(event); self.moveCursor(QTextCursor.End)
def paintEvent(self, e):
super().paintEvent(e)
painter = QPainter(self.viewport())
grad = QLinearGradient(0, 0, 0, 30)
bg = self.palette().color(QPalette.ColorRole.Base)
grad.setColorAt(0.0, QColor(bg.red(), bg.green(), bg.blue(), 255))
grad.setColorAt(1.0, QColor(bg.red(), bg.green(), bg.blue(), 0))
painter.fillRect(0, 0, self.viewport().width(), 30, grad)
painter.end()
# ── Images (optional backends) ────────────────────────────────────────────────
def have_requests_or_raise():
if requests is None:
raise RuntimeError("The 'requests' library is required for image fetching. pip install requests")
def fetch_image_stability(prompt: str) -> Optional[bytes]:
have_requests_or_raise(); assert requests is not None
api_key = os.getenv("STABILITY_API_KEY", "").strip()
if not api_key: raise RuntimeError("STABILITY_API_KEY is not set")
url = "https://api.stability.ai/v2beta/stable-image/generate/core"
headers = {"Authorization": f"Bearer {api_key}", "Accept": "image/png"}
data = {"prompt": prompt[:1000], "output_format": "png"}
resp = requests.post(url, headers=headers, data=data, timeout=60)
if resp.status_code == 200 and resp.content: return resp.content
raise RuntimeError(f"Stability API error: {resp.status_code} {resp.text[:200]}")
def fetch_image_pexels(query: str) -> Optional[bytes]:
have_requests_or_raise(); assert requests is not None
api_key = os.getenv("PEXELS_API_KEY", "").strip()
if not api_key: raise RuntimeError("PEXELS_API_KEY is not set")
s_url = "https://api.pexels.com/v1/search"
headers = {"Authorization": api_key}
params = {"query": query, "per_page": 1}
r = requests.get(s_url, headers=headers, params=params, timeout=30); r.raise_for_status()
data = r.json(); photos = data.get("photos") or []
if not photos: return None
src = photos[0].get("src") or {}
raw_url = src.get("original") or src.get("large") or src.get("medium")
if not raw_url: return None
img = requests.get(raw_url, timeout=60); img.raise_for_status()
return img.content
# ── PlantUML ──────────────────────────────────────────────────────────────────
def ensure_plantuml_present():
exe = shutil.which("plantuml")
if not exe: raise RuntimeError("plantuml not found in PATH. sudo apt install plantuml")
return exe
def render_plantuml_to_png_bytes(uml_text: str) -> bytes:
exe = ensure_plantuml_present()
proc = subprocess.run([exe, "-tpng", "-pipe"], input=uml_text.encode("utf-8"),
stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False)
if proc.returncode != 0 or not proc.stdout:
raise RuntimeError(f"plantuml failed: {proc.stderr.decode('utf-8','ignore')[:200]}")
return proc.stdout
# ── Background worker ─────────────────────────────────────────────────────────
class GenWorker(QRunnable):
class Signals(QObject):
finished = Signal(dict); error = Signal(str)
def __init__(self, provider: RambleProvider, payload: Dict[str, Any]):
super().__init__(); self.provider = provider; self.payload = payload
self.signals = GenWorker.Signals()
def run(self):
try:
data = self.provider.generate(**self.payload)
self.signals.finished.emit(data) # type: ignore
except Exception as e:
self.signals.error.emit(str(e)) # type: ignore
# ── Dialog ────────────────────────────────────────────────────────────────────
@dataclass
class RambleResult:
summary: str
fields: Dict[str, str]
class RambleDialog(QDialog):
def __init__(
self,
*,
prompt: str,
fields: List[str],
field_criteria: Optional[Dict[str,str]] = None,
hints: Optional[List[str]] = None,
provider: Optional[RambleProvider] = None,
enable_stability: bool = False,
enable_pexels: bool = False,
parent: Optional[QWidget] = None
):
super().__init__(parent)
self.setWindowTitle("Ramble → Generate")
self.resize(1120, 760)
self.provider = provider or cast(RambleProvider, MockProvider())
self._prompt_text = prompt
self._fields = fields[:] if fields else ["Summary"]
if "Summary" not in self._fields:
self._fields.insert(0, "Summary")
self._criteria = field_criteria or {}
# sensible default for Summary if not provided
self._criteria.setdefault("Summary", "<= 2 sentences")
self._hints = hints or [
"What is it called?",
"Who benefits most?",
"What problem does it solve?",
"What would success look like?",
]
self.enable_stability = enable_stability
self.enable_pexels = enable_pexels and not enable_stability
self.thread_pool = QThreadPool.globalInstance()
self.result: Optional[RambleResult] = None
# Track per-field lock states
self.field_lock_boxes: Dict[str, QCheckBox] = {}
self.field_outputs: Dict[str, QTextEdit] = {}
# Layout: scrollable content + fixed footer
outer = QVBoxLayout(self)
self.scroll = QScrollArea(); self.scroll.setWidgetResizable(True)
content = QWidget(); self.scroll.setWidget(content)
outer.addWidget(self.scroll, 1)
footer = QHBoxLayout()
# “How to use” toggle
self.help_btn = QPushButton("How to use ▾"); self.help_btn.setCheckable(True); self.help_btn.setChecked(False)
self.help_btn.clicked.connect(self._toggle_help) # type: ignore
footer.addWidget(self.help_btn)
footer.addStretch(1)
self.submit_btn = QPushButton("Submit"); self.submit_btn.clicked.connect(self.on_submit) # type: ignore
footer.addWidget(self.submit_btn)
outer.addLayout(footer)
grid = QGridLayout(content)
grid.setHorizontalSpacing(16); grid.setVerticalSpacing(12)
# Title & hint
title = QLabel("<b>Ramble about your idea. Fields will fill themselves.</b>")
title.setWordWrap(True)
grid.addWidget(title, 0, 0, 1, 2)
self.hint_label = FadeLabel(); self.hint_label.setText(self._hints[0])
self.hint_label.setStyleSheet("color:#666; font-style:italic;")
grid.addWidget(self.hint_label, 1, 0, 1, 2)
# Left column: Ramble → Generate → UML → Images
left_col = QVBoxLayout()
self.ramble_edit = FadingRambleEdit(max_blocks=900)
left_col.addWidget(self.ramble_edit)
gen_row = QHBoxLayout()
self.generate_btn = QPushButton("Generate / Update")
self.generate_btn.setStyleSheet("QPushButton { background:#e6f4ff; border:1px solid #9ed6ff; padding:6px 12px; }")
self.generate_btn.clicked.connect(self.on_generate) # type: ignore
self.provider_label = QLabel(f"Provider: {type(self.provider).__name__}")
self.provider_label.setStyleSheet("color:#555;")
gen_row.addWidget(self.generate_btn); gen_row.addStretch(1); gen_row.addWidget(self.provider_label)
left_col.addLayout(gen_row)
# UML (image only)
uml_group = QGroupBox("Diagram (PlantUML)"); uml_v = QVBoxLayout(uml_group)
self.uml_image_label = QLabel(); self.uml_image_label.setAlignment(Qt.AlignCenter)
if QT_LIB == "PyQt5":
self.uml_image_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
else:
self.uml_image_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
uml_v.addWidget(self.uml_image_label)
left_col.addWidget(uml_group)
# Images / descriptions
img_group = QGroupBox("Images / Descriptions"); img_v = QVBoxLayout(img_group)
self.img_desc_text = QTextEdit(); self.img_desc_text.setReadOnly(True); self.img_desc_text.setMinimumHeight(80)
self.img_desc_text.setPlaceholderText("Descriptions of suggested images…")
img_v.addWidget(self.img_desc_text)
left_col.addWidget(img_group)
# Right column: Fields
right_col = QVBoxLayout()
fields_group = QGroupBox("Fields")
fields_group.setStyleSheet("QGroupBox { background:#fafafa; border:1px solid #ddd; border-radius:8px; margin-top:6px; }")
fg_form = QFormLayout(fields_group)
def apply_lock_style(widget: QTextEdit, locked: bool):
widget.setStyleSheet("background-color: #fff7cc;" if locked else "")
for f in self._fields:
row = QHBoxLayout()
te = QTextEdit(); te.setReadOnly(False); te.setMinimumHeight(64)
te.setPlaceholderText(f"{f}")
self.field_outputs[f] = te
lock = QCheckBox("Lock"); lock.setChecked(False)
self.field_lock_boxes[f] = lock
lock.stateChanged.connect(lambda _=0, name=f: apply_lock_style(self.field_outputs[name], self.field_lock_boxes[name].isChecked())) # type: ignore
# show criteria hint (subtle)
crit = self._criteria.get(f, "")
if crit:
hint = QLabel(f"<span style='color:#777; font-size:11px'>({crit})</span>")
else:
hint = QLabel("")
row.addWidget(te, 1); row.addWidget(lock)
fg_form.addRow(QLabel(f"{f}:"), row)
fg_form.addRow(hint)
apply_lock_style(te, False)
right_col.addWidget(fields_group, 1)
# Inline help panel (hidden by default)
self.help_panel = QLabel(
"How to use:\n"
"• Start in the blue box above and just ramble about your idea.\n"
"• Click “Generate / Update” to fill fields automatically.\n"
"• Not happy? Add more detail to your ramble or edit fields directly.\n"
"• Love a field? Tick “Lock” so it wont change next time. Locked fields also help guide the next generation.\n"
"• When satisfied, click Submit to output your structured JSON."
)
self.help_panel.setWordWrap(True)
self.help_panel.setVisible(False)
self.help_panel.setStyleSheet("background:#f8fbff; border:1px dashed #bcdcff; padding:8px; border-radius:6px;")
right_col.addWidget(self.help_panel)
# Status
self.status_label = QLabel(""); self.status_label.setStyleSheet("color:#777;")
right_col.addWidget(self.status_label)
# Place columns
left_w = QWidget(); left_w.setLayout(left_col)
right_w = QWidget(); right_w.setLayout(right_col)
grid.addWidget(left_w, 2, 0)
grid.addWidget(right_w, 2, 1)
self._setup_hint_timer()
def _toggle_help(self):
self.help_panel.setVisible(self.help_btn.isChecked())
self.help_btn.setText("How to use ▴" if self.help_btn.isChecked() else "How to use ▾")
def _setup_hint_timer(self):
self._hint_idx = 0
self._hint_timer = QTimer(self); self._hint_timer.setInterval(3500)
self._hint_timer.timeout.connect(self._advance_hint) # type: ignore
self._hint_timer.start()
def _advance_hint(self):
if not self._hints: return
self._hint_idx = (self._hint_idx + 1) % len(self._hints)
self.hint_label.fade_to_text(self._hints[self._hint_idx])
@Slot()
def on_generate(self):
ramble_text = self.ramble_edit.toPlainText()
if not ramble_text.strip():
QMessageBox.information(self, "Nothing to generate", "Type your thoughts first, then click Generate / Update.")
return
# Collect locked fields to feed back as authoritative context
locked_context: Dict[str, str] = {}
for name, box in self.field_lock_boxes.items():
if box.isChecked():
locked_context[name] = self.field_outputs[name].toPlainText().strip()
payload = {
"prompt": self._prompt_text,
"ramble_text": ramble_text,
"fields": self._fields,
"field_criteria": self._criteria,
"locked_context": locked_context,
}
self._set_busy(True, "Generating…")
worker = GenWorker(self.provider, payload)
worker.signals.finished.connect(self._on_generated) # type: ignore
worker.signals.error.connect(self._on_gen_error) # type: ignore
self.thread_pool.start(worker)
def _on_generated(self, data: Dict[str, Any]):
# Respect locks
new_fields: Dict[str, str] = data.get("fields", {})
for name, widget in self.field_outputs.items():
if self.field_lock_boxes[name].isChecked():
continue
widget.setPlainText(new_fields.get(name, ""))
# Also update Summary if present and not locked
if "Summary" in self.field_outputs and not self.field_lock_boxes["Summary"].isChecked():
# If provider also returns top-level "summary", prefer that
s = data.get("summary", "") or new_fields.get("Summary", "")
if s: self.field_outputs["Summary"].setPlainText(s)
# UML render (first diagram)
uml_blocks = data.get("uml_blocks", [])
self.uml_image_label.clear()
for (uml_text, maybe_png) in uml_blocks:
png_bytes = maybe_png
if not png_bytes and uml_text:
try:
png_bytes = render_plantuml_to_png_bytes(uml_text)
except Exception as e:
print(f"[UML render] {e}", file=sys.stderr)
png_bytes = None
if png_bytes:
pix = QPixmap()
if pix.loadFromData(png_bytes):
self.uml_image_label.setPixmap(pix)
break
# Images / descriptions
img_desc = [str(s) for s in data.get("image_descriptions", [])]
self.img_desc_text.setPlainText("\n\n".join(f"{d}" for d in img_desc))
self._set_busy(False, "Ready.")
def _on_gen_error(self, msg: str):
self._set_busy(False, "Error.")
QMessageBox.critical(self, "Generation Error", msg)
def _set_busy(self, busy: bool, msg: str):
self.generate_btn.setEnabled(not busy)
self.submit_btn.setEnabled(not busy)
self.status_label.setText(msg)
@Slot()
def on_submit(self):
out_fields = {k: self.field_outputs[k].toPlainText() for k in self.field_outputs.keys()}
# prefer explicit Summary field if present
summary = out_fields.get("Summary", "").strip()
self.result = RambleResult(summary=summary, fields=out_fields)
self.accept()
# ── Public API ────────────────────────────────────────────────────────────────
def open_ramble_dialog(
*,
prompt: str,
fields: List[str],
field_criteria: Optional[Dict[str, str]] = None,
hints: Optional[List[str]] = None,
provider: Optional[RambleProvider] = None,
enable_stability: bool = False,
enable_pexels: bool = False,
parent: Optional[QWidget] = None
) -> Optional[Dict[str, Any]]:
app_created = False
app = QApplication.instance()
if app is None:
app_created = True
app = QApplication(sys.argv)
dlg = RambleDialog(
prompt=prompt,
fields=fields,
field_criteria=field_criteria,
hints=hints,
provider=provider,
enable_stability=enable_stability,
enable_pexels=enable_pexels,
parent=parent
)
rc = dlg.exec_() if QT_LIB == "PyQt5" else dlg.exec()
out = None
if rc == QDialog.Accepted and dlg.result:
out = {
"summary": dlg.result.summary,
"fields": dlg.result.fields,
}
if app_created:
app.quit()
return out
# ── CLI demo ──────────────────────────────────────────────────────────────────
def parse_args():
p = argparse.ArgumentParser(description="Ramble → Generate (PlantUML + optional images)")
p.add_argument("--provider", choices=["mock", "claude"], default="mock")
p.add_argument("--claude-cmd", default="claude", help="Path to claude CLI")
p.add_argument("--stability", action="store_true", help="Enable Stability AI images (needs STABILITY_API_KEY)")
p.add_argument("--pexels", action="store_true", help="Enable Pexels images (needs PEXELS_API_KEY); ignored if --stability set")
p.add_argument("--prompt", default="Explain your new feature idea")
p.add_argument("--fields", nargs="+", default=["Summary","Title","Intent","ProblemItSolves","BriefOverview"])
p.add_argument("--criteria", default="", help="JSON mapping of field -> criteria")
p.add_argument("--hints", default="", help="JSON list of hint strings")
p.add_argument("--timeout", type=int, default=90)
p.add_argument("--tail", type=int, default=6000)
p.add_argument("--debug", action="store_true")
return p.parse_args()
if __name__ == "__main__":
args = parse_args()
# Field criteria parsing
criteria: Dict[str, str] = {}
if args.criteria.strip():
try:
criteria = json.loads(args.criteria)
except Exception as e:
print(f"[WARN] Could not parse --criteria JSON: {e}", file=sys.stderr)
# Provider
if args.provider == "claude":
provider = cast(RambleProvider, ClaudeCLIProvider(
cmd=args.claude_cmd, use_arg_p=True, timeout_s=args.timeout, tail_chars=args.tail, debug=args.debug,
))
else:
provider = cast(RambleProvider, MockProvider())
# Ensure PlantUML
try:
ensure_plantuml_present()
except Exception as e:
print(f"[FATAL] {e}", file=sys.stderr); sys.exit(2)
if (args.stability or args.pexels) and requests is None:
print("[FATAL] 'requests' is required for image backends. pip install requests", file=sys.stderr)
sys.exit(3)
# Parse JSON args (tolerate empty/invalid)
try:
criteria = json.loads(args.criteria) if args.criteria else {}
if not isinstance(criteria, dict): criteria = {}
except Exception:
criteria = {}
try:
hints = json.loads(args.hints) if args.hints else None
if hints is not None and not isinstance(hints, list): hints = None
except Exception:
hints = None
demo = open_ramble_dialog(
prompt=args.prompt,
fields=args.fields,
field_criteria=criteria,
hints = hints,
provider=provider,
enable_stability=args.stability,
enable_pexels=args.pexels,
)
if demo:
print(json.dumps(demo, ensure_ascii=False, indent=2))

View File

@ -1,27 +0,0 @@
# Project Guide
## Core idea
- An **empty projects first feature defines the whole project**.
Subsequent features extend that foundation.
## Daily flow
1. Create or update features under `Docs/features/FR_*/...`.
2. Commit. The pre-commit hook **drives the discussion** and **maintains summaries** (within marker blocks).
3. Discuss in `Docs/features/.../discussions/*.discussion.md` and **end each comment with**
`VOTE: READY` or `CHANGES` or `REJECT`.
## First run
- After installation, make an initial commit to activate the hook:
```bash
git add .
git commit -m "chore: initial commit"
```
## Start a new feature (recommended)
- Copy process/templates/feature_request.md to Docs/features/FR_YYYY-MM-DD_<slug>/request.md
- Fill in: Intent, Motivation, Constraints, Rough Proposal, Open Questions, Author
- Commit; the system will drive the discussion and generate/maintain summaries automatically.
## Notes
- Keep discussions append-only; votes are single-line VOTE: markers.
- Human READY is required at Implementation/Release stages.
- Ramble (ramble.py) is optional; it can extract fields from your free-form notes.

View File

@ -1,9 +0,0 @@
# Design — <FR id / Title>
## Context & Goals
## Non-Goals & Constraints
## Options Considered
## Decision & Rationale
## Architecture Diagram(s)
## Risks & Mitigations
## Acceptance Criteria (measurable)

View File

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

View File

@ -1,44 +0,0 @@
<!--META
{
"kind": "discussion_summary",
"tokens": ["FeatureId", "CreatedDate"]
}
-->
# Summary — Feature {FeatureId}
<!-- SUMMARY:DECISIONS START -->
## Decisions (ADR-style)
- (none yet)
<!-- SUMMARY:DECISIONS END -->
<!-- SUMMARY:OPEN_QUESTIONS START -->
## Open Questions
- (none yet)
<!-- SUMMARY:OPEN_QUESTIONS END -->
<!-- SUMMARY:AWAITING START -->
## Awaiting Replies
- (none yet)
<!-- SUMMARY:AWAITING END -->
<!-- SUMMARY:ACTION_ITEMS START -->
## Action Items
- (none yet)
<!-- SUMMARY:ACTION_ITEMS END -->
<!-- SUMMARY:VOTES START -->
## Votes (latest per participant)
READY: 1 • CHANGES: 0 • REJECT: 0
- Maintainer
<!-- SUMMARY:VOTES END -->
<!-- SUMMARY:TIMELINE START -->
## Timeline (most recent first)
- {CreatedDate} Maintainer: Kickoff
<!-- SUMMARY:TIMELINE END -->
<!-- SUMMARY:LINKS START -->
## Links
- Design/Plan: ../design/design.md
<!-- SUMMARY:LINKS END -->

View File

@ -1,35 +0,0 @@
# Feature Request: <title>
<!--META
{
"kind": "feature_request",
"ramble_fields": [
{"name": "Title", "hint": "camelCase, ≤24 chars", "default": "initialProjectDesign"},
{"name": "Intent"},
{"name": "ProblemItSolves"},
{"name": "BriefOverview"},
{"name": "Summary", "hint": "≤2 sentences"}
],
"criteria": {
"Title": "camelCase, <= 24 chars",
"Summary": "<= 2 sentences"
},
"hints": [
"What is it called?",
"Who benefits most?",
"What problem does it solve?",
"What does success look like?"
],
"tokens": ["FeatureId", "CreatedDate", "Title", "Intent", "ProblemItSolves", "BriefOverview", "Summary"]
}
-->
# Feature Request: {Title}
**Intent**: {Intent}
**Motivation / Problem**: {ProblemItSolves}
**Brief Overview**: {BriefOverview}
**Summary**: {Summary}
**Meta**: FeatureId: {FeatureId} • Created: {CreatedDate}

View File

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

View File

@ -1,27 +0,0 @@
# Python
__pycache__/
*.py[cod]
*.pyo
*.pyd
*.egg-info/
.pytest_cache/
.mypy_cache/
.coverage
htmlcov/
# Node (if any JS tooling appears)
node_modules/
dist/
build/
# Env / secrets
.env
.env.*
secrets/
# OS/editor
.DS_Store
Thumbs.db
# Project
.git/ai-rules-*

View File

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

View File

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

View File

@ -1,107 +0,0 @@
# CascadingDev Simplified - Docker Compose
#
# Quick Start:
# docker-compose build # Build the image
# docker-compose run --rm cli # Show help
# docker-compose run --rm build # Build installer bundle
# docker-compose run --rm shell # Interactive shell
version: '3.8'
services:
# ============================================================================
# CLI (show help)
# ============================================================================
cli:
build:
context: .
dockerfile: Dockerfile
image: cascadingdev:latest
command: ["cdev", "--help"]
# ============================================================================
# Build installer bundle
# ============================================================================
build:
build:
context: .
dockerfile: Dockerfile
image: cascadingdev:latest
volumes:
- ./output:/output
command: ["bash", "-c", "cdev build && cp -r install/* /output/ && echo 'Installer bundle copied to ./output/'"]
# ============================================================================
# Test installation (creates project in /tmp)
# ============================================================================
test-install:
build:
context: .
dockerfile: Dockerfile
image: cascadingdev:latest
command: >
bash -c "
cdev build &&
python install/cascadingdev-*/setup_cascadingdev.py --target /tmp/test-project --no-ramble &&
echo '=== Installation successful ===' &&
ls -la /tmp/test-project/ &&
echo '=== Pre-commit hook ===' &&
cat /tmp/test-project/.git/hooks/pre-commit | head -30
"
# ============================================================================
# GUI (requires X11 forwarding)
# ============================================================================
gui:
build:
context: .
dockerfile: Dockerfile
image: cascadingdev:latest
environment:
- DISPLAY=${DISPLAY:-:0}
- QT_QPA_PLATFORM=xcb
volumes:
- /tmp/.X11-unix:/tmp/.X11-unix:ro
- ./output:/output
command: >
bash -c "
cdev build &&
python install/cascadingdev-*/setup_cascadingdev.py --target /output/test-project
"
network_mode: host
# ============================================================================
# Interactive Shell
# ============================================================================
shell:
build:
context: .
dockerfile: Dockerfile
image: cascadingdev:latest
volumes:
- ./output:/output
command: ["/bin/bash"]
stdin_open: true
tty: true
# ==============================================================================
# Usage Examples
# ==============================================================================
#
# Build image:
# docker-compose build
#
# Show CLI help:
# docker-compose run --rm cli
#
# Build installer bundle (outputs to ./output/):
# docker-compose run --rm build
#
# Test installation in container:
# docker-compose run --rm test-install
#
# Interactive shell:
# docker-compose run --rm shell
#
# GUI with Ramble (requires: xhost +local:docker):
# docker-compose run --rm gui

File diff suppressed because it is too large Load Diff

View File

@ -1,28 +0,0 @@
# CascadingDev Installer
## Requirements
- Python 3.10+ and git
- (Optional) PySide6 for GUI (`pip install PySide6`)
## Quick start
```bash
python setup_cascadingdev.py --target /path/to/new-project
```
### Skip GUI
```bash
python setup_cascadingdev.py --target /path/to/new-project --no-ramble
```
> After installation, open `USER_GUIDE.md` in your new project for daily usage.
## Rebuild & Run (for maintainers)
Rebuild the bundle every time you change assets/ or the installer:
```bash
python tools/build_installer.py
```
Then run only the bundled copy:
```bash
python install/cascadingdev-*/setup_cascadingdev.py --target /path/to/new-project
```

42
index.html Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,21 +0,0 @@
# pyproject.toml
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "cascadingdev"
# Tell PEP 621 that version is provided dynamically
dynamic = ["version"]
description = "CascadingDev: scaffold rule-driven multi-agent project repos"
requires-python = ">=3.10"
[project.scripts]
cdev = "cascadingdev.cli:main"
[tool.setuptools]
package-dir = {"" = "src"}
packages = ["cascadingdev"]
[tool.setuptools.dynamic]
version = { file = "VERSION" }

1
sitemap.xml Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"><url><loc>https://pages.brrd.tech/rob/CascadingDev_simplified/</loc><changefreq>weekly</changefreq><priority>0.5</priority></url></urlset>

View File

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

View File

@ -1,82 +0,0 @@
# src/cascadingdev/cli.py
import argparse, sys, shutil
from pathlib import Path
from . import __version__
from .utils import ROOT, read_version, bump_version, run
def main():
ap = argparse.ArgumentParser(prog="cascadingdev", description="CascadingDev CLI")
ap.add_argument("--version", action="store_true", help="Show version and exit")
sub = ap.add_subparsers(dest="cmd")
sub.add_parser("doctor", help="Check environment and templates")
sub.add_parser("smoke", help="Run smoke test")
p_build = sub.add_parser("build", help="Build installer bundle (no version bump)")
p_rel = sub.add_parser("release", help="Bump version and rebuild")
p_rel.add_argument("--kind", choices=["major","minor","patch"], default="patch")
p_pack = sub.add_parser("pack", help="Zip the current installer bundle")
p_pack.add_argument("--out", help="Output zip path (default: ./install/cascadingdev-<ver>.zip)")
p_bs = sub.add_parser("bundle-smoke", help="Unpack the zip and run installer into a temp dir")
p_bs.add_argument("--keep", action="store_true")
p_bs.add_argument("--ramble", action="store_true")
p_bs.add_argument("--bundle", help="Path to installer zip")
p_bs.add_argument("--target", help="Write demo repo to this path")
args = ap.parse_args()
if args.version:
print(__version__)
return 0
if args.cmd == "doctor":
# minimal checks
required = [
ROOT / "assets" / "templates" / "USER_GUIDE.md",
ROOT / "assets" / "templates" / "process" / "policies.yml",
ROOT / "assets" / "templates" / "rules" / "root.ai-rules.yml",
ROOT / "assets" / "templates" / "rules" / "features.ai-rules.yml",
ROOT / "assets" / "hooks" / "pre-commit",
ROOT / "src" / "cascadingdev" / "setup_project.py",
]
missing = [str(p) for p in required if not p.exists()]
if missing:
print("Missing:\n " + "\n ".join(missing)); return 2
print("Doctor OK."); return 0
if args.cmd == "smoke":
return run([sys.executable, str(ROOT / "tools" / "smoke_test.py")])
if args.cmd == "build":
return run([sys.executable, str(ROOT / "tools" / "build_installer.py")])
if args.cmd == "release":
newv = bump_version(args.kind, ROOT / "VERSION")
print(f"Bumped to {newv}")
rc = run([sys.executable, str(ROOT / "tools" / "build_installer.py")])
if rc == 0:
print(f"Built installer for {newv}")
return rc
if args.cmd == "pack":
ver = read_version(ROOT / "VERSION")
bundle = ROOT / "install" / f"cascadingdev-{ver}"
if not bundle.exists():
print(f"Bundle not found: {bundle}. Run `cascadingdev build` first.")
return 2
out = Path(args.out) if args.out else (ROOT / "install" / f"cascadingdev-{ver}.zip")
if out.exists():
out.unlink()
shutil.make_archive(out.with_suffix(""), "zip", root_dir=bundle)
print(f"Packed → {out}")
return 0
if args.cmd == "bundle-smoke":
cmd = [sys.executable, str(ROOT / "tools" / "bundle_smoke.py")]
if args.keep: cmd.append("--keep")
if args.ramble: cmd.append("--ramble")
if args.bundle: cmd += ["--bundle", args.bundle]
if args.target: cmd += ["--target", args.target]
return run(cmd)
ap.print_help()
return 0

View File

@ -1,456 +0,0 @@
#!/usr/bin/env python3
"""
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:
- Prompts (or use --target) for the destination repo path
- Copies essential files from installer target (ramble.py, templates, hooks)
- Creates canonical structure, seeds rules/templates
- Initializes git and installs pre-commit hook
- Launches Ramble to capture the first feature request
Examples:
python setup_cascadingdev.py --target ~/dev/my-new-repo
python setup_cascadingdev.py --target /abs/path --no-ramble
"""
import json, re
import argparse
import datetime
import sys
import subprocess
import shutil
from pathlib import Path
# Bundle root (must contain assets/, ramble.py, VERSION)
INSTALL_ROOT = Path(__file__).resolve().parent
if not (INSTALL_ROOT / "assets").exists():
print("[-] This script must be run from the installer bundle directory (assets/ missing).")
print(
" Rebuild the bundle (e.g., `python tools/build_installer.py`) and run the copy in install/cascadingdev-*/.")
sys.exit(2)
# ---------- Helper Functions ----------
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:
ensure_dir(dst.parent)
if not dst.exists():
shutil.copy2(src, dst)
def run(cmd: list[str], cwd: Path | None = None) -> int:
proc = subprocess.Popen(cmd, cwd=cwd, stdout=sys.stdout, stderr=sys.stderr)
return proc.wait()
# --- Tiny template helpers ----------------------------------------------------
# Self-contained; no external dependencies
_META_RE = re.compile(r"<!--META\s*(\{.*?\})\s*-->", re.S)
def load_template_with_meta(path: Path) -> tuple[dict, str]:
"""
Returns (meta: dict, body_without_meta: str). If no META, ({} , full text).
META must be a single JSON object inside <!--META ... -->.
"""
if not path.exists():
return {}, ""
text = path.read_text(encoding="utf-8")
m = _META_RE.search(text)
if not m:
return {}, text
meta_json = m.group(1)
try:
meta = json.loads(meta_json)
except Exception:
meta = {}
body = _META_RE.sub("", text, count=1).lstrip()
return meta, body
def render_placeholders(body: str, values: dict) -> str:
"""
Simple {Token} replacement. Leaves unknown tokens as-is.
"""
# two-pass: {{Token}} then {Token}
out = body
for k, v in values.items():
out = out.replace("{{" + k + "}}", str(v))
try:
out = out.format_map({k: v for k, v in values.items()})
except Exception:
pass
return out
def meta_ramble_config(meta: dict) -> tuple[list[str], dict, dict, list[str]]:
"""
From template META, extract:
- fields: list of field names in order
- defaults: {field: default_value}
- criteria: {field: rule/description} (optional)
- hints: [str, ...] (optional)
"""
fields: list[str] = []
defaults: dict = {}
for spec in meta.get("ramble_fields", []):
name = spec.get("name")
if name:
fields.append(name)
if "default" in spec:
defaults[name] = spec["default"]
criteria = meta.get("criteria", {}) or {}
hints = meta.get("hints", []) or []
return fields, defaults, criteria, hints
def ensure_git_repo(target: Path):
"""Initialize a git repository if one doesn't exist at the target path."""
if not (target / ".git").exists():
# Initialize git repo with main branch
run(["git", "init", "-b", "main"], cwd=target)
# Seed .gitignore from template if present; otherwise fallback
tmpl_gitignore = INSTALL_ROOT / "assets" / "templates" / "root_gitignore"
if tmpl_gitignore.exists():
copy_if_missing(tmpl_gitignore, target / ".gitignore")
else:
write_if_missing(target / ".gitignore", "\n".join([
"__pycache__/", "*.py[cod]", "*.egg-info/", ".pytest_cache/",
".mypy_cache/", ".coverage", "htmlcov/", "node_modules/",
"dist/", "build/", ".env", ".env.*", "secrets/", ".DS_Store",
".git/ai-rules-*",
]) + "\n")
def install_precommit_hook(target: Path):
"""Install the pre-commit hook from installer assets to target git hooks."""
hook_src = INSTALL_ROOT / "assets" / "hooks" / "pre-commit"
hooks_dir = target / ".git" / "hooks"
hooks_dir.mkdir(parents=True, exist_ok=True)
hook_dst = hooks_dir / "pre-commit"
if not hook_src.exists():
say("[-] pre-commit hook source missing at assets/hooks/pre-commit in the installer bundle.")
return
# Copy hook content and make it executable
hook_dst.write_text(hook_src.read_text(encoding="utf-8"), encoding="utf-8")
hook_dst.chmod(0o755)
say(f"[+] Installed git hook → {hook_dst}")
def run_ramble_and_collect(target: Path, provider: str = "mock", claude_cmd: str = "claude"):
"""
Launch Ramble GUI to collect initial feature request details.
Falls back to terminal prompts if GUI fails or returns invalid JSON.
"""
# Find FR template + read META (for field names)
fr_tmpl = INSTALL_ROOT / "assets" / "templates" / "feature_request.md"
meta, _ = load_template_with_meta(fr_tmpl)
field_names, _defaults, criteria, hints = meta_ramble_config(meta)
# Fallback to your previous default fields if template lacks META
if not field_names:
field_names = ["Summary", "Title", "Intent", "ProblemItSolves", "BriefOverview"]
ramble = target / "ramble.py"
if not ramble.exists():
say("[-] ramble.py not found in target; skipping interactive FR capture.")
return None
# Build Ramble arguments dynamically from the template-defined fields
args = [
sys.executable, str(ramble),
"--provider", provider,
"--claude-cmd", claude_cmd,
"--prompt", "Describe your initial feature request for this repository",
"--fields", *field_names,
]
if criteria:
args += ["--criteria", json.dumps(criteria)]
if hints:
args += ["--hints", json.dumps(hints)]
say("[•] Launching Ramble (close the dialog with Submit to return JSON)…")
proc = subprocess.run(args, text=True, capture_output=True, cwd=str(target))
# Show any stderr output from Ramble
if proc.stderr and proc.stderr.strip():
say("[Ramble stderr]")
say(proc.stderr.strip())
# Try to parse JSON output from Ramble
out = (proc.stdout or "").strip()
if out:
try:
return json.loads(out)
except Exception as e:
say(f"[-] JSON parse failed: {e}")
# Terminal fallback - collect input manually if GUI fails
say("[!] Falling back to terminal prompts.")
def ask(label, default=""):
try:
v = input(f"{label}: ").strip()
return v or default
except EOFError:
return default
# Collect required fields via terminal input
fields = {
"Title": ask("Title (camelCase, <=24 chars)", "initialProjectDesign"),
"Intent": ask("Intent", ""),
"ProblemItSolves": ask("Problem it solves", ""),
"BriefOverview": ask("Brief overview", ""),
"Summary": ask("One- or two-sentence summary", ""),
}
return {"fields": fields, "summary": fields.get("Summary", "")}
def seed_process_and_rules(target: Path):
"""Seed machine-readable policies and stage rules by copying installer templates."""
# Seed process/policies.yml (machine-readable), per DESIGN.md Appendix A
process_dir = target / "process"
rules_dir = target / "Docs" / "features"
process_dir.mkdir(parents=True, exist_ok=True)
rules_dir.mkdir(parents=True, exist_ok=True)
# Locate templates in THIS installer bundle
t_root = INSTALL_ROOT / "assets" / "templates"
t_process = t_root / "process" / "policies.yml"
t_rules_root = t_root / "rules" / "root.ai-rules.yml"
t_rules_features = t_root / "rules" / "features.ai-rules.yml"
# Copy policies
if t_process.exists():
copy_if_missing(t_process, process_dir / "policies.yml")
# Copy rules files into expected locations
# Root rules (optional if you want a project-wide baseline)
if t_rules_root.exists():
copy_if_missing(t_rules_root, target / ".ai-rules.yml")
# Discussion/feature rules (cascade/override within Docs/features)
if t_rules_features.exists():
copy_if_missing(t_rules_features, rules_dir / ".ai-rules.yml")
def seed_initial_feature(target: Path, req_fields: dict | None):
today = datetime.date.today().isoformat()
feature_id = f"FR_{today}_initial-feature-request"
fr_dir = target / "Docs" / "features" / feature_id
disc_dir = fr_dir / "discussions"
disc_dir.mkdir(parents=True, exist_ok=True)
# Gather values from Ramble result (if any)
fields = (req_fields or {}).get("fields", {}) if req_fields else {}
# Load FR template + META
fr_tmpl = INSTALL_ROOT / "assets" / "templates" / "feature_request.md"
fr_meta, fr_body = load_template_with_meta(fr_tmpl)
field_names, defaults, _criteria, _hints = meta_ramble_config(fr_meta)
# Build values map with defaults → ramble fields → system tokens
values = {}
values.update(defaults) # template defaults
values.update(fields) # user-entered
values.update({ # system tokens
"FeatureId": feature_id,
"CreatedDate": today,
})
# If no template body, fall back to your old default
if not fr_body.strip():
title = values.get("Title", "initialProjectDesign")
intent = values.get("Intent", "")
problem = values.get("ProblemItSolves", "")
brief = values.get("BriefOverview", "")
summary = values.get("Summary", "")
fr_body = f"""# Feature Request: {title}
**Intent**: {intent}
**Motivation / Problem**: {problem}
**Brief Overview**: {brief}
**Summary**: {summary}
**Meta**: Created: {today}
"""
(fr_dir / "request.md").write_text(render_placeholders(fr_body, values), encoding="utf-8")
# --- feature.discussion.md ---
disc_tmpl = INSTALL_ROOT / "assets" / "templates" / "feature.discussion.md"
d_meta, d_body = load_template_with_meta(disc_tmpl)
# Always include the front-matter for rules, then template body (or fallback)
fm = f"""---\ntype: discussion\nstage: feature\nstatus: OPEN\nfeature_id: {feature_id}\ncreated: {today}\n---\n"""
if not d_body.strip():
d_body = (
"## Summary\n"
f"Initial discussion for {feature_id}. Append your comments below.\n\n"
"## Participation\n"
"- Maintainer: Kickoff. VOTE: READY\n"
)
(disc_dir / "feature.discussion.md").write_text(fm + render_placeholders(d_body, values), encoding="utf-8")
# --- feature.discussion.sum.md ---
sum_tmpl = INSTALL_ROOT / "assets" / "templates" / "feature.discussion.sum.md"
s_meta, s_body = load_template_with_meta(sum_tmpl)
if s_body.strip():
# use template
(disc_dir / "feature.discussion.sum.md").write_text(render_placeholders(s_body, values), encoding="utf-8")
else:
# your existing static content
(disc_dir / "feature.discussion.sum.md").write_text(
"""# 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)
- {ts} Maintainer: Kickoff
<!-- SUMMARY:TIMELINE END -->
<!-- SUMMARY:LINKS START -->
## Links
- Design/Plan: ../design/design.md
<!-- SUMMARY:LINKS END -->
""".replace("{ts}", today), encoding="utf-8")
def copy_install_assets_to_target(target: Path):
"""Copy essential files from the installer to the target repository."""
# Runtime helpers into project root
copy_if_exists(INSTALL_ROOT / "ramble.py", target / "ramble.py")
copy_if_exists(INSTALL_ROOT / "create_feature.py", target / "create_feature.py")
# User guide into project root
copy_if_exists(INSTALL_ROOT / "assets" / "templates" / "USER_GUIDE.md",
target / "USER_GUIDE.md")
# Copy shipped templates (preferred source of truth)
tmpl_src = INSTALL_ROOT / "assets" / "templates"
if tmpl_src.exists():
shutil.copytree(tmpl_src, target / "process" / "templates", dirs_exist_ok=True)
# Place USER_GUIDE.md under process/ (clear separation from source templates)
ug_src = tmpl_src / "USER_GUIDE.md"
if ug_src.exists():
(target / "process").mkdir(parents=True, exist_ok=True)
shutil.copy2(ug_src, target / "process" / "USER_GUIDE.md")
# Hook is installed into .git/hooks by install_precommit_hook()
# Optionally copy any additional assets you drop under installer/automation, etc.
# Example: copy starter automation folder if provided in installer
if (INSTALL_ROOT / "automation").exists():
shutil.copytree(INSTALL_ROOT / "automation", target / "automation", dirs_exist_ok=True)
def first_commit(target: Path):
"""Perform the initial git commit of all scaffolded files."""
try:
run(["git", "add", "-A"], cwd=target)
run(["git", "commit", "-m", "chore: bootstrap Cascading Development scaffolding"], cwd=target)
except Exception:
# Silently continue if commit fails (e.g., no git config)
pass
def main():
"""Main entry point for the Cascading Development setup script."""
# Parse command line arguments
ap = argparse.ArgumentParser()
ap.add_argument("--target", help="Destination path to create/use the repo")
ap.add_argument("--provider", choices=["mock", "claude"], default="mock", help="Ramble provider (default: mock)")
ap.add_argument("--no-ramble", action="store_true", help="Skip launching Ramble")
ap.add_argument("--claude-cmd", default="claude")
args = ap.parse_args()
# Get target directory from args or prompt user
target_str = args.target
if not target_str:
target_str = input("Destination repo path (will be created if missing): ").strip()
if not target_str:
say("No target specified. Aborting.")
sys.exit(2)
# Resolve and create target directory
target = Path(target_str).expanduser().resolve()
target.mkdir(parents=True, exist_ok=True)
say(f"[=] Installing Cascading Development into: {target}")
# Step 1: Copy assets from installer into target
copy_install_assets_to_target(target)
# Step 2: Create standard folder structure
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)
# Step 3: Create rules/templates and basic process docs
seed_process_and_rules(target)
# Step 4: Initialize git & install pre-commit
ensure_git_repo(target)
install_precommit_hook(target)
# Step 5: Launch Ramble (if available and not disabled)
req = None
if not args.no_ramble:
req = run_ramble_and_collect(target, provider=args.provider, claude_cmd=args.claude_cmd)
# Step 6: Seed first feature based on Ramble output
seed_initial_feature(target, req)
# Step 7: Perform initial commit
first_commit(target)
# Completion message
say("[✓] Setup complete.")
say(f"Next steps:\n cd {target}\n git status")
if __name__ == "__main__":
main()

View File

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

View File

@ -1,53 +0,0 @@
#!/usr/bin/env python3
import shutil, os
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "install"
VER = (ROOT / "VERSION").read_text().strip() if (ROOT / "VERSION").exists() else "0.1.0"
BUNDLE = OUT / f"cascadingdev-{VER}"
def main():
# Removes the old install bundle if it already exists
if BUNDLE.exists():
shutil.rmtree(BUNDLE)
# Create the directories
(BUNDLE / "assets" / "hooks").mkdir(parents=True, exist_ok=True)
(BUNDLE / "assets" / "templates").mkdir(parents=True, exist_ok=True)
# Copy the git hook and any other runtime utilities.
shutil.copy2(ROOT / "assets" / "runtime" / "ramble.py", BUNDLE / "ramble.py")
shutil.copy2(ROOT / "assets" / "runtime" / "create_feature.py", BUNDLE / "create_feature.py")
shutil.copy2(ROOT / "assets" / "hooks" / "pre-commit", BUNDLE / "assets" / "hooks" / "pre-commit")
# copy core templates
for t in [
"feature_request.md",
"feature.discussion.md",
"feature.discussion.sum.md",
"design_doc.md",
"USER_GUIDE.md",
"root_gitignore",
]:
shutil.copy2(ROOT / "assets" / "templates" / t, BUNDLE / "assets" / "templates" / t)
# copy (recursively) the contents of process/ and rules/ templates folders
def copy_tree(src, dst):
if dst.exists():
shutil.rmtree(dst)
shutil.copytree(src, dst)
copy_tree(ROOT / "assets" / "templates" / "process", BUNDLE / "assets" / "templates" / "process")
copy_tree(ROOT / "assets" / "templates" / "rules", BUNDLE / "assets" / "templates" / "rules")
# write installer entrypoint
shutil.copy2(ROOT / "src" / "cascadingdev" / "setup_project.py",
BUNDLE / "setup_cascadingdev.py")
(BUNDLE / "INSTALL.md").write_text("Unzip, then run:\n\n python3 setup_cascadingdev.py\n", encoding="utf-8")
(BUNDLE / "VERSION").write_text(VER, encoding="utf-8")
print(f"[✓] Built installer → {BUNDLE}")
if __name__ == "__main__":
main()

View File

@ -1,74 +0,0 @@
#!/usr/bin/env python3
import tempfile, shutil, subprocess, sys, argparse
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--bundle", help="Path to a specific installer zip (defaults to install/cascadingdev-<VERSION>.zip)")
ap.add_argument("--keep", action="store_true", help="Keep temporary directory for inspection")
ap.add_argument("--target", help="Write the demo repo to this path instead of a temp dir")
ap.add_argument("--ramble", action="store_true", help="Run installer without --no-ramble")
args = ap.parse_args()
ver = (ROOT / "VERSION").read_text().strip()
zip_path = Path(args.bundle) if args.bundle else (ROOT / "install" / f"cascadingdev-{ver}.zip")
if not zip_path.exists():
print(f"Zip missing: {zip_path}. Run `cdev pack` first or pass --bundle."); sys.exit(2)
def run_once(extract_dir: Path, target_dir: Path) -> int:
shutil.unpack_archive(str(zip_path), str(extract_dir), "zip")
setup = next(extract_dir.rglob("setup_cascadingdev.py"))
target_dir.mkdir(parents=True, exist_ok=True)
print(f"[•] Running installer from: {setup}")
cmd = [sys.executable, str(setup), "--target", str(target_dir)]
if not args.ramble:
cmd.append("--no-ramble")
rc = subprocess.call(cmd)
if rc != 0:
print(f"Installer exited with {rc}"); return rc
# quick asserts
required = [
target_dir / "process" / "policies.yml",
target_dir / "Docs" / "features" / ".ai-rules.yml",
target_dir / ".ai-rules.yml",
target_dir / "USER_GUIDE.md",
target_dir / ".git" / "hooks" / "pre-commit",
]
missing = [str(p) for p in required if not p.exists()]
if missing:
print("Missing after install:\n " + "\n ".join(missing)); return 3
print(f"[✓] Bundle smoke OK. Demo repo: {target_dir}")
return 0
if args.target:
# Use a fixed location (never auto-delete). Clean if exists.
target_dir = Path(args.target).expanduser().resolve()
if target_dir.exists():
shutil.rmtree(target_dir)
extract_dir = target_dir.parent / (target_dir.name + "-bundle")
if extract_dir.exists():
shutil.rmtree(extract_dir)
extract_dir.mkdir(parents=True, exist_ok=True)
rc = run_once(extract_dir, target_dir)
print(f"[i] Extracted bundle kept at: {extract_dir}")
return rc
# Temp-mode (default)
with tempfile.TemporaryDirectory(prefix="cd-bundle-") as tmp:
tmpdir = Path(tmp)
extract_dir = tmpdir / "bundle"
target_dir = tmpdir / "demo-repo"
rc = run_once(extract_dir, target_dir)
if args.keep:
print(f"[i] Keeping temp dir: {tmpdir}")
print(" You can inspect it now; press Enter to clean up...")
try: input()
except EOFError: pass
return rc
# auto-clean
return rc
if __name__ == "__main__":
sys.exit(main())

View File

@ -1,22 +0,0 @@
#!/usr/bin/env python3
from pathlib import Path
def main():
root = Path(__file__).resolve().parents[1]
required = [
root / "assets" / "hooks" / "pre-commit",
root / "assets" / "templates" / "feature_request.md",
root / "assets" / "templates" / "feature.discussion.md",
root / "assets" / "templates" / "design_doc.md",
root / "assets" / "templates" / "USER_GUIDE.md", # now required
root / "assets" / "runtime" / "ramble.py",
root / "tools" / "build_installer.py",
root / "src" / "cascadingdev" / "setup_project.py",
]
missing = [str(p) for p in required if not p.exists()]
if missing:
print("Missing:", *missing, sep="\n ")
raise SystemExit(2)
print("Smoke OK.")
if __name__ == "__main__":
main()