From d78b0d83c8ff8ef7e0f366c21651055313d9b88f Mon Sep 17 00:00:00 2001 From: rob Date: Wed, 29 Oct 2025 01:22:40 -0300 Subject: [PATCH] 1st commit --- assets/runtime/create_feature.py | 2 +- assets/runtime/ramble.py | 15 +- assets/templates/discussion.md | 25 --- assets/templates/feature.discussion.md | 12 ++ assets/templates/feature.discussion.sum.md | 44 +++++ assets/templates/feature_request.md | 41 ++++- assets/templates/root_gitignore | 27 +++ docs/DESIGN.md | 20 +-- src/cascadingdev/setup_project.py | 181 ++++++++++++++++----- tools/build_installer.py | 10 +- tools/smoke_test.py | 2 +- 11 files changed, 289 insertions(+), 90 deletions(-) delete mode 100644 assets/templates/discussion.md create mode 100644 assets/templates/feature.discussion.md create mode 100644 assets/templates/feature.discussion.sum.md create mode 100644 assets/templates/root_gitignore diff --git a/assets/runtime/create_feature.py b/assets/runtime/create_feature.py index c72b774..42fb31e 100644 --- a/assets/runtime/create_feature.py +++ b/assets/runtime/create_feature.py @@ -146,7 +146,7 @@ Initial discussion for feature `{fid}`. Append your comments below. ## Participation - Maintainer: Kickoff. VOTE: READY """ - write_text(dir_disc / "feature.discussion.md", req) + write_text(dir_disc / "feature.feature.discussion.md", req) sum_md = f"""# Summary — Feature diff --git a/assets/runtime/ramble.py b/assets/runtime/ramble.py index feaddd6..ad9040e 100644 --- a/assets/runtime/ramble.py +++ b/assets/runtime/ramble.py @@ -699,6 +699,7 @@ def parse_args(): p.add_argument("--prompt", default="Explain your new feature idea") p.add_argument("--fields", nargs="+", default=["Summary","Title","Intent","ProblemItSolves","BriefOverview"]) p.add_argument("--criteria", default="", help="JSON mapping of field -> criteria") + p.add_argument("--hints", default="", help="JSON list of hint strings") p.add_argument("--timeout", type=int, default=90) p.add_argument("--tail", type=int, default=6000) p.add_argument("--debug", action="store_true") @@ -733,11 +734,23 @@ if __name__ == "__main__": print("[FATAL] 'requests' is required for image backends. pip install requests", file=sys.stderr) sys.exit(3) + # Parse JSON args (tolerate empty/invalid) + try: + criteria = json.loads(args.criteria) if args.criteria else {} + if not isinstance(criteria, dict): criteria = {} + except Exception: + criteria = {} + try: + hints = json.loads(args.hints) if args.hints else None + if hints is not None and not isinstance(hints, list): hints = None + except Exception: + hints = None + demo = open_ramble_dialog( prompt=args.prompt, fields=args.fields, field_criteria=criteria, - hints=None, + hints = hints, provider=provider, enable_stability=args.stability, enable_pexels=args.pexels, diff --git a/assets/templates/discussion.md b/assets/templates/discussion.md deleted file mode 100644 index ec92d52..0000000 --- a/assets/templates/discussion.md +++ /dev/null @@ -1,25 +0,0 @@ -type: discussion -stage: -status: OPEN -feature_id: -created: - -promotion_rule: - allow_agent_votes: true - ready_min_eligible_votes: all - reject_min_eligible_votes: all - -participation: - instructions: | - - Append your input at the end as: "YourName: your comment…" - - Every comment must end with a vote line: "VOTE: READY|CHANGES|REJECT" - - Agents/bots must prefix names with "AI_" - -voting: - values: [READY, CHANGES, REJECT] ---- -## Summary -2-4 sentence summary of current state - -## Participation -comments appended below \ No newline at end of file diff --git a/assets/templates/feature.discussion.md b/assets/templates/feature.discussion.md new file mode 100644 index 0000000..7751e73 --- /dev/null +++ b/assets/templates/feature.discussion.md @@ -0,0 +1,12 @@ + + +## Summary +Initial discussion for {FeatureId}. Append your comments below. + +## Participation +- Maintainer: Kickoff. VOTE: READY diff --git a/assets/templates/feature.discussion.sum.md b/assets/templates/feature.discussion.sum.md new file mode 100644 index 0000000..9d33377 --- /dev/null +++ b/assets/templates/feature.discussion.sum.md @@ -0,0 +1,44 @@ + + +# Summary — Feature {FeatureId} + + +## Decisions (ADR-style) +- (none yet) + + + +## Open Questions +- (none yet) + + + +## Awaiting Replies +- (none yet) + + + +## Action Items +- (none yet) + + + +## Votes (latest per participant) +READY: 1 • CHANGES: 0 • REJECT: 0 +- Maintainer + + + +## Timeline (most recent first) +- {CreatedDate} Maintainer: Kickoff + + + +## Links +- Design/Plan: ../design/design.md + diff --git a/assets/templates/feature_request.md b/assets/templates/feature_request.md index 9d61d5e..8370dd5 100644 --- a/assets/templates/feature_request.md +++ b/assets/templates/feature_request.md @@ -1,10 +1,35 @@ # Feature Request: -**Feature ID**: <FR_YYYY-MM-DD_slug> -**Intent**: <one paragraph describing purpose> -**Motivation / Problem**: <why this is needed now> -**Constraints / Non-Goals**: <bulleted list of limitations> -**Rough Proposal**: <short implementation outline> -**Open Questions**: <bulleted list of uncertainties> -**Meta**: Created: <date> • Author: <name> -Discussion Template (process/templates/discussion.md): \ No newline at end of file +<!--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} diff --git a/assets/templates/root_gitignore b/assets/templates/root_gitignore new file mode 100644 index 0000000..2b5b140 --- /dev/null +++ b/assets/templates/root_gitignore @@ -0,0 +1,27 @@ +# Python +__pycache__/ +*.py[cod] +*.pyo +*.pyd +*.egg-info/ +.pytest_cache/ +.mypy_cache/ +.coverage +htmlcov/ + +# Node (if any JS tooling appears) +node_modules/ +dist/ +build/ + +# Env / secrets +.env +.env.* +secrets/ + +# OS/editor +.DS_Store +Thumbs.db + +# Project +.git/ai-rules-* diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 9dd06b7..1152fef 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -850,7 +850,7 @@ rules: feature_request: outputs: feature_discussion: - path: "{dir}/discussions/feature.discussion.md" + path: "{dir}/discussions/feature.feature.discussion.md" output_type: "feature_discussion_writer" instruction: | If missing: create with standard header (stage: feature, status: OPEN), @@ -870,7 +870,7 @@ rules: outputs: # 1) Append the new AI comment to the discussion (append-only) self_append: - path: "{dir}/discussions/feature.discussion.md" + path: "{dir}/discussions/feature.feature.discussion.md" output_type: "feature_discussion_writer" instruction: | Append concise comment signed with AI name, ending with a single vote line. @@ -890,7 +890,7 @@ rules: # 3) Promotion artifacts when READY_FOR_DESIGN design_discussion: - path: "{dir}/discussions/design.discussion.md" + path: "{dir}/discussions/design.feature.discussion.md" output_type: "design_discussion_writer" instruction: | Create ONLY if feature discussion status is READY_FOR_DESIGN. @@ -930,7 +930,7 @@ rules: Update only the marker-bounded sections from the discussion content. impl_discussion: - path: "{dir}/discussions/implementation.discussion.md" + path: "{dir}/discussions/implementation.feature.discussion.md" output_type: "impl_discussion_writer" instruction: | Create ONLY if design discussion status is READY_FOR_IMPLEMENTATION. @@ -974,7 +974,7 @@ rules: Include unchecked items from ../implementation/tasks.md in ACTION_ITEMS. test_discussion: - path: "{dir}/discussions/testing.discussion.md" + path: "{dir}/discussions/testing.feature.discussion.md" output_type: "test_discussion_writer" instruction: | Create ONLY if implementation status is READY_FOR_TESTING. @@ -1024,7 +1024,7 @@ rules: Initialize bug discussion and fix plan in the same folder. review_discussion: - path: "{dir}/discussions/review.discussion.md" + path: "{dir}/discussions/review.feature.discussion.md" output_type: "review_discussion_writer" instruction: | Create ONLY if all test checklist items pass. @@ -1141,7 +1141,7 @@ resolve_template() { ext="${basename##*.}" # nearest FR_* ancestor as feature_id feature_id="$(echo "$rel_path" | sed -n 's|.*Docs/features/\(FR_[^/]*\).*|\1|p')" - # infer stage from <stage>.discussion.md when applicable + # infer stage from <stage>.feature.discussion.md when applicable stage="$(echo "$basename" | sed -n 's/^\([A-Za-z0-9_-]\+\)\.discussion\.md$/\1/p')" echo "$tmpl" \ | sed -e "s|{date}|$today|g" \ @@ -1288,7 +1288,7 @@ Rule Definition (in Docs/features/.ai-rules.yml): discussion_moderator_nudge: outputs: self_append: - path: "{dir}/discussions/{stage}.discussion.md" + path: "{dir}/discussions/{stage}.feature.discussion.md" output_type: "discussion_moderator_writer" instruction: | Act as AI_Moderator. Analyze the entire discussion and: @@ -1425,7 +1425,7 @@ Bypass & Minimal Patch: ```bash .git/ai-rules-debug/ -├─ 20251021-143022-12345-feature.discussion.md/ +├─ 20251021-143022-12345-feature.feature.discussion.md/ │ ├─ raw.out # Raw model output │ ├─ clean.diff # Extracted patch │ ├─ sanitized.diff # After sanitization @@ -1975,7 +1975,7 @@ Docs/features/FR_.../ type: discussion-summary stage: feature # feature|design|implementation|testing|review status: ACTIVE # ACTIVE|SNAPSHOT|ARCHIVED -source_discussion: feature.discussion.md +source_discussion: feature.feature.discussion.md feature_id: FR_YYYY-MM-DD_<slug> updated: YYYY-MM-DDTHH:MM:SSZ policy: diff --git a/src/cascadingdev/setup_project.py b/src/cascadingdev/setup_project.py index 20ccc3c..2ce8a43 100644 --- a/src/cascadingdev/setup_project.py +++ b/src/cascadingdev/setup_project.py @@ -13,7 +13,7 @@ Examples: python setup_cascadingdev.py --target ~/dev/my-new-repo python setup_cascadingdev.py --target /abs/path --no-ramble """ -import json +import json, re import argparse import datetime import sys @@ -57,16 +57,79 @@ 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) - # Create basic .gitignore file - write_if_missing(target / ".gitignore", "\n".join([ - ".env", ".env.*", "secrets/", ".git/ai-rules-*", "__pycache__/", - "*.pyc", ".pytest_cache/", ".DS_Store", - ]) + "\n") + # 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.""" @@ -90,21 +153,34 @@ def run_ramble_and_collect(target: Path, provider: str = "mock", claude_cmd: str 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 command arguments + # Build Ramble arguments dynamically from the template-defined fields args = [ sys.executable, str(ramble), "--provider", provider, "--claude-cmd", claude_cmd, "--prompt", "Describe your initial feature request for this repository", - "--fields", "Summary", "Title", "Intent", "ProblemItSolves", "BriefOverview", - "--criteria", '{"Summary":"<= 2 sentences","Title":"camelCase, <= 24 chars"}' + "--fields", *field_names, ] + if criteria: + args += ["--criteria", json.dumps(criteria)] + if hints: + args += ["--hints", json.dumps(hints)] + say("[•] Launching Ramble (close the dialog with Submit to return JSON)…") proc = subprocess.run(args, text=True, capture_output=True, cwd=str(target)) @@ -170,53 +246,72 @@ def seed_process_and_rules(target: Path): copy_if_missing(t_rules_features, rules_dir / ".ai-rules.yml") def seed_initial_feature(target: Path, req_fields: dict | None): - """Create the initial feature request and associated discussion files.""" today = datetime.date.today().isoformat() - fr_dir = target / "Docs" / "features" / f"FR_{today}_initial-feature-request" + feature_id = f"FR_{today}_initial-feature-request" + fr_dir = target / "Docs" / "features" / feature_id disc_dir = fr_dir / "discussions" disc_dir.mkdir(parents=True, exist_ok=True) - # Create feature request content, using Ramble data if available - if req_fields: - title = (req_fields.get("fields", {}) or {}).get("Title", "").strip() or "initialProjectDesign" - intent = (req_fields.get("fields", {}) or {}).get("Intent", "").strip() or "—" - problem = (req_fields.get("fields", {}) or {}).get("ProblemItSolves", "").strip() or "—" - brief = (req_fields.get("fields", {}) or {}).get("BriefOverview", "").strip() or "—" - summary = (req_fields.get("summary") or "").strip() - body = f"""# Feature Request: {title} + # Gather values from Ramble result (if any) + fields = (req_fields or {}).get("fields", {}) if req_fields else {} + # Load FR template + META + fr_tmpl = INSTALL_ROOT / "assets" / "templates" / "feature_request.md" + fr_meta, fr_body = load_template_with_meta(fr_tmpl) + field_names, defaults, _criteria, _hints = meta_ramble_config(fr_meta) + + # Build values map with defaults → ramble fields → system tokens + values = {} + values.update(defaults) # template defaults + values.update(fields) # user-entered + values.update({ # system tokens + "FeatureId": feature_id, + "CreatedDate": today, + }) + + # If no template body, fall back to your old default + if not fr_body.strip(): + title = values.get("Title", "initialProjectDesign") + intent = values.get("Intent", "—") + problem = values.get("ProblemItSolves", "—") + brief = values.get("BriefOverview", "—") + summary = values.get("Summary", "") + fr_body = f"""# Feature Request: {title} **Intent**: {intent} **Motivation / Problem**: {problem} **Brief Overview**: {brief} **Summary**: {summary} + **Meta**: Created: {today} """ + + (fr_dir / "request.md").write_text(render_placeholders(fr_body, values), encoding="utf-8") + + # --- feature.discussion.md --- + disc_tmpl = INSTALL_ROOT / "assets" / "templates" / "feature.discussion.md" + d_meta, d_body = load_template_with_meta(disc_tmpl) + # Always include the front-matter for rules, then template body (or fallback) + fm = f"""---\ntype: discussion\nstage: feature\nstatus: OPEN\nfeature_id: {feature_id}\ncreated: {today}\n---\n""" + if not d_body.strip(): + d_body = ( + "## Summary\n" + f"Initial discussion for {feature_id}. Append your comments below.\n\n" + "## Participation\n" + "- Maintainer: Kickoff. VOTE: READY\n" + ) + (disc_dir / "feature.discussion.md").write_text(fm + render_placeholders(d_body, values), encoding="utf-8") + + # --- feature.discussion.sum.md --- + sum_tmpl = INSTALL_ROOT / "assets" / "templates" / "feature.discussion.sum.md" + s_meta, s_body = load_template_with_meta(sum_tmpl) + if s_body.strip(): + # use template + (disc_dir / "feature.discussion.sum.md").write_text(render_placeholders(s_body, values), encoding="utf-8") else: - # Fallback to template content if no Ramble data - body = (target / "process" / "templates" / "feature_request.md").read_text(encoding="utf-8") - - (fr_dir / "request.md").write_text(body, encoding="utf-8") - - # Create initial discussion file - (disc_dir / "feature.discussion.md").write_text( - f"""--- -type: discussion -stage: feature -status: OPEN -feature_id: FR_{today}_initial-feature-request -created: {today} ---- -## Summary -Initial discussion for the first feature request. Append your comments below. - -## Participation -- Maintainer: Kickoff. VOTE: READY -""", encoding="utf-8") - - # Create companion summary file with structured sections - (disc_dir / "feature.discussion.sum.md").write_text( - """# Summary — Feature + # your existing static content + (disc_dir / "feature.discussion.sum.md").write_text( + """# Summary — Feature <!-- SUMMARY:DECISIONS START --> ## Decisions (ADR-style) diff --git a/tools/build_installer.py b/tools/build_installer.py index f4a8c58..9260200 100644 --- a/tools/build_installer.py +++ b/tools/build_installer.py @@ -22,7 +22,15 @@ def main(): shutil.copy2(ROOT / "assets" / "hooks" / "pre-commit", BUNDLE / "assets" / "hooks" / "pre-commit") # copy core templates - for t in ["feature_request.md","discussion.md","design_doc.md","USER_GUIDE.md"]: + + 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 diff --git a/tools/smoke_test.py b/tools/smoke_test.py index cec35db..2b1bb5f 100644 --- a/tools/smoke_test.py +++ b/tools/smoke_test.py @@ -6,7 +6,7 @@ def main(): required = [ root / "assets" / "hooks" / "pre-commit", root / "assets" / "templates" / "feature_request.md", - root / "assets" / "templates" / "discussion.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",