1st commit

This commit is contained in:
rob 2025-10-29 01:22:40 -03:00
parent db728ac2e4
commit d78b0d83c8
11 changed files with 289 additions and 90 deletions

View File

@ -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

View File

@ -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,

View File

@ -1,25 +0,0 @@
type: discussion
stage: <feature|design|implementation|testing|review>
status: OPEN
feature_id: <FR_...>
created: <YYYY-MM-DD>
promotion_rule:
allow_agent_votes: true
ready_min_eligible_votes: all
reject_min_eligible_votes: all
participation:
instructions: |
- Append your input at the end as: "YourName: your comment…"
- Every comment must end with a vote line: "VOTE: READY|CHANGES|REJECT"
- Agents/bots must prefix names with "AI_"
voting:
values: [READY, CHANGES, REJECT]
---
## Summary
2-4 sentence summary of current state
## Participation
comments appended below

View File

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

View File

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

View File

@ -1,10 +1,35 @@
# Feature Request: <title>
**Feature ID**: <FR_YYYY-MM-DD_slug>
**Intent**: <one paragraph describing purpose>
**Motivation / Problem**: <why this is needed now>
**Constraints / Non-Goals**: <bulleted list of limitations>
**Rough Proposal**: <short implementation outline>
**Open Questions**: <bulleted list of uncertainties>
**Meta**: Created: <date> • Author: <name>
Discussion Template (process/templates/discussion.md):
<!--META
{
"kind": "feature_request",
"ramble_fields": [
{"name": "Title", "hint": "camelCase, ≤24 chars", "default": "initialProjectDesign"},
{"name": "Intent"},
{"name": "ProblemItSolves"},
{"name": "BriefOverview"},
{"name": "Summary", "hint": "≤2 sentences"}
],
"criteria": {
"Title": "camelCase, <= 24 chars",
"Summary": "<= 2 sentences"
},
"hints": [
"What is it called?",
"Who benefits most?",
"What problem does it solve?",
"What does success look like?"
],
"tokens": ["FeatureId", "CreatedDate", "Title", "Intent", "ProblemItSolves", "BriefOverview", "Summary"]
}
-->
# Feature Request: {Title}
**Intent**: {Intent}
**Motivation / Problem**: {ProblemItSolves}
**Brief Overview**: {BriefOverview}
**Summary**: {Summary}
**Meta**: FeatureId: {FeatureId} • Created: {CreatedDate}

View File

@ -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-*

View File

@ -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:

View File

@ -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,15 +57,78 @@ 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
# Seed .gitignore from template if present; otherwise fallback
tmpl_gitignore = INSTALL_ROOT / "assets" / "templates" / "root_gitignore"
if tmpl_gitignore.exists():
copy_if_missing(tmpl_gitignore, target / ".gitignore")
else:
write_if_missing(target / ".gitignore", "\n".join([
".env", ".env.*", "secrets/", ".git/ai-rules-*", "__pycache__/",
"*.pyc", ".pytest_cache/", ".DS_Store",
"__pycache__/", "*.py[cod]", "*.egg-info/", ".pytest_cache/",
".mypy_cache/", ".coverage", "htmlcov/", "node_modules/",
"dist/", "build/", ".env", ".env.*", "secrets/", ".DS_Store",
".git/ai-rules-*",
]) + "\n")
def install_precommit_hook(target: Path):
@ -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,51 +246,70 @@ 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
# your existing static content
(disc_dir / "feature.discussion.sum.md").write_text(
"""# Summary — Feature

View File

@ -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

View File

@ -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",