1st commit
This commit is contained in:
parent
1684169515
commit
c61c988966
|
|
@ -0,0 +1,81 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo ".")"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
# -------- 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) ----------
|
||||||
|
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) ----------
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
for f in "${STAGED[@]}"; do
|
||||||
|
case "$f" in
|
||||||
|
Docs/features/*/discussions/*.discussion.md) ensure_summary "$f";;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# -------- future orchestration (non-blocking status) ----------
|
||||||
|
if [ -x "automation/workflow.py" ]; then
|
||||||
|
python3 automation/workflow.py --status || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
|
@ -0,0 +1,310 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
setup_project.py — Installer-mode bootstrap for Cascading Development
|
||||||
|
|
||||||
|
Run this from your installation folder (NOT inside the destination repo):
|
||||||
|
- Prompts (or use --target) for the destination repo path
|
||||||
|
- Copies essential files from installer → target (DESIGN.md, ramble.py, hooks)
|
||||||
|
- Creates canonical structure, seeds rules/templates
|
||||||
|
- Initializes git and installs pre-commit hook
|
||||||
|
- Launches Ramble to capture the first feature request
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
python3 scripts/setup_project.py
|
||||||
|
python3 scripts/setup_project.py --target ~/dev/my-new-repo
|
||||||
|
python3 scripts/setup_project.py --target /abs/path --no-ramble
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import argparse
|
||||||
|
import subprocess
|
||||||
|
import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
INSTALL_ROOT = Path(__file__).resolve().parent.parent # installer root (contains this scripts/ dir)
|
||||||
|
|
||||||
|
# ---------- helpers ----------
|
||||||
|
def sh(cmd, check=True, cwd=None):
|
||||||
|
return subprocess.run(cmd, check=check, text=True, capture_output=True, cwd=cwd)
|
||||||
|
|
||||||
|
def say(msg): print(msg, flush=True)
|
||||||
|
|
||||||
|
def write_if_missing(path: Path, content: str):
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
if not path.exists():
|
||||||
|
path.write_text(content, encoding="utf-8")
|
||||||
|
|
||||||
|
def copy_if_exists(src: Path, dst: Path):
|
||||||
|
if src.exists():
|
||||||
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy2(str(src), str(dst))
|
||||||
|
|
||||||
|
def ensure_git_repo(target: Path):
|
||||||
|
if not (target / ".git").exists():
|
||||||
|
sh(["git", "init", "-b", "main"], cwd=str(target))
|
||||||
|
write_if_missing(target / ".gitignore", "\n".join([
|
||||||
|
".env", ".env.*", "secrets/", ".git/ai-rules-*", "__pycache__/",
|
||||||
|
"*.pyc", ".pytest_cache/", ".DS_Store",
|
||||||
|
]) + "\n")
|
||||||
|
|
||||||
|
def install_precommit_hook(target: Path):
|
||||||
|
hook_src = INSTALL_ROOT / "scripts" / "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 scripts/hooks/pre-commit in the installer.")
|
||||||
|
return
|
||||||
|
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"):
|
||||||
|
ramble = target / "ramble.py"
|
||||||
|
if not ramble.exists():
|
||||||
|
say("[-] ramble.py not found in target; skipping interactive FR capture.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
args = [
|
||||||
|
sys.executable, str(ramble),
|
||||||
|
"--provider", provider,
|
||||||
|
"--prompt", "Describe your initial feature request for this repository",
|
||||||
|
"--fields", "Summary", "Title", "Intent", "ProblemItSolves", "BriefOverview",
|
||||||
|
"--criteria", '{"Summary":"<= 2 sentences","Title":"camelCase, <= 24 chars"}'
|
||||||
|
]
|
||||||
|
say("[•] Launching Ramble (close the dialog to continue)…")
|
||||||
|
proc = subprocess.run(args, text=True, capture_output=True, cwd=str(target))
|
||||||
|
out = (proc.stdout or "").strip()
|
||||||
|
try:
|
||||||
|
return json.loads(out)
|
||||||
|
except Exception:
|
||||||
|
say("[-] Could not parse Ramble output; proceeding with template text.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def seed_process_and_rules(target: Path):
|
||||||
|
write_if_missing(target / "process" / "design.md",
|
||||||
|
"# Process & Architecture (Local Notes)\n\n(See DESIGN.md for full spec.)\n")
|
||||||
|
write_if_missing(target / "process" / "policies.md",
|
||||||
|
"# Policies (Human-readable)\n\nSee machine-readable config in policies.yml.\n")
|
||||||
|
write_if_missing(target / "process" / "policies.yml",
|
||||||
|
"""version: 1
|
||||||
|
voting:
|
||||||
|
values: [READY, CHANGES, REJECT]
|
||||||
|
allow_agent_votes: true
|
||||||
|
quorum:
|
||||||
|
discussion: { ready: all, reject: all }
|
||||||
|
implementation: { ready: 1_human, reject: all }
|
||||||
|
review: { ready: 1_human, reject: all }
|
||||||
|
eligibility:
|
||||||
|
agents_allowed: true
|
||||||
|
require_human_for: [implementation, review]
|
||||||
|
etiquette:
|
||||||
|
name_prefix_agents: "AI_"
|
||||||
|
vote_line_regex: "^VOTE:\\s*(READY|CHANGES|REJECT)\\s*$"
|
||||||
|
response_timeout_hours: 24
|
||||||
|
""")
|
||||||
|
|
||||||
|
tmpl_dir = target / "process" / "templates"
|
||||||
|
write_if_missing(tmpl_dir / "feature_request.md",
|
||||||
|
"# Feature Request: <title>\n\n**Intent**: …\n**Motivation / Problem**: …\n**Constraints / Non-Goals**: …\n**Rough Proposal**: …\n**Open Questions**: …\n")
|
||||||
|
write_if_missing(tmpl_dir / "discussion.md",
|
||||||
|
"---\ntype: discussion\nstage: <feature|design|implementation|testing|review>\nstatus: OPEN\ncreated: <YYYY-MM-DD>\n---\n\n## Summary\n\n## Participation\n")
|
||||||
|
write_if_missing(tmpl_dir / "design_doc.md",
|
||||||
|
"# Design — <FR id / Title>\n\n## Context & Goals\n## Non-Goals & Constraints\n## Options Considered\n## Decision & Rationale\n## Architecture Diagram(s)\n## Risks & Mitigations\n## Acceptance Criteria\n")
|
||||||
|
|
||||||
|
write_if_missing(target / ".ai-rules.yml",
|
||||||
|
"""version: 1
|
||||||
|
file_associations:
|
||||||
|
"*.md": "md-file"
|
||||||
|
|
||||||
|
rules:
|
||||||
|
md-file:
|
||||||
|
description: "Normalize Markdown"
|
||||||
|
instruction: |
|
||||||
|
Keep markdown tidy (headings, lists, spacing). No content churn.
|
||||||
|
settings:
|
||||||
|
model: "local-mock"
|
||||||
|
temperature: 0.1
|
||||||
|
""")
|
||||||
|
|
||||||
|
write_if_missing(target / "Docs" / "features" / ".ai-rules.yml",
|
||||||
|
"""version: 1
|
||||||
|
file_associations:
|
||||||
|
"feature.discussion.md": "feature_discussion"
|
||||||
|
"feature.discussion.sum.md": "discussion_summary"
|
||||||
|
|
||||||
|
rules:
|
||||||
|
feature_discussion:
|
||||||
|
outputs:
|
||||||
|
summary_companion:
|
||||||
|
path: "{dir}/discussions/feature.discussion.sum.md"
|
||||||
|
output_type: "discussion_summary_writer"
|
||||||
|
instruction: |
|
||||||
|
Ensure the summary file exists and maintain only the bounded sections:
|
||||||
|
DECISIONS, OPEN_QUESTIONS, AWAITING, ACTION_ITEMS, VOTES, TIMELINE, LINKS.
|
||||||
|
|
||||||
|
discussion_summary:
|
||||||
|
outputs:
|
||||||
|
normalize:
|
||||||
|
path: "{dir}/feature.discussion.sum.md"
|
||||||
|
output_type: "discussion_summary_normalizer"
|
||||||
|
instruction: |
|
||||||
|
If missing, create summary with standard markers. Do not edit text outside markers.
|
||||||
|
""")
|
||||||
|
|
||||||
|
def seed_initial_feature(target: Path, req_fields: dict | None):
|
||||||
|
today = datetime.date.today().isoformat()
|
||||||
|
fr_dir = target / "Docs" / "features" / f"FR_{today}_initial-feature-request"
|
||||||
|
disc_dir = fr_dir / "discussions"
|
||||||
|
disc_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if req_fields:
|
||||||
|
title = (req_fields.get("fields", {}) or {}).get("Title", "").strip() or "initialProjectDesign"
|
||||||
|
intent = (req_fields.get("fields", {}) or {}).get("Intent", "").strip() or "—"
|
||||||
|
problem = (req_fields.get("fields", {}) or {}).get("ProblemItSolves", "").strip() or "—"
|
||||||
|
brief = (req_fields.get("fields", {}) or {}).get("BriefOverview", "").strip() or "—"
|
||||||
|
summary = (req_fields.get("summary") or "").strip()
|
||||||
|
body = f"""# Feature Request: {title}
|
||||||
|
|
||||||
|
**Intent**: {intent}
|
||||||
|
**Motivation / Problem**: {problem}
|
||||||
|
**Brief Overview**: {brief}
|
||||||
|
|
||||||
|
**Summary**: {summary}
|
||||||
|
**Meta**: Created: {today}
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
body = (target / "process" / "templates" / "feature_request.md").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
(fr_dir / "request.md").write_text(body, encoding="utf-8")
|
||||||
|
|
||||||
|
(disc_dir / "feature.discussion.md").write_text(
|
||||||
|
f"""---
|
||||||
|
type: discussion
|
||||||
|
stage: feature
|
||||||
|
status: OPEN
|
||||||
|
feature_id: FR_{today}_initial-feature-request
|
||||||
|
created: {today}
|
||||||
|
---
|
||||||
|
## Summary
|
||||||
|
Initial discussion for the first feature request. Append your comments below.
|
||||||
|
|
||||||
|
## Participation
|
||||||
|
- Maintainer: Kickoff. VOTE: READY
|
||||||
|
""", encoding="utf-8")
|
||||||
|
|
||||||
|
(disc_dir / "feature.discussion.sum.md").write_text(
|
||||||
|
"""# Summary — Feature
|
||||||
|
|
||||||
|
<!-- 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 DESIGN.md and ramble.py from installer if present
|
||||||
|
copy_if_exists(INSTALL_ROOT / "DESIGN.md", target / "DESIGN.md")
|
||||||
|
copy_if_exists(INSTALL_ROOT / "ramble.py", target / "ramble.py")
|
||||||
|
# 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):
|
||||||
|
try:
|
||||||
|
sh(["git", "add", "-A"], cwd=str(target))
|
||||||
|
sh(["git", "commit", "-m", "chore: bootstrap Cascading Development scaffolding"], cwd=str(target))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--target", help="Destination path to create/use the repo")
|
||||||
|
ap.add_argument("--provider", default="mock", help="Ramble provider (default: mock)")
|
||||||
|
ap.add_argument("--no-ramble", action="store_true", help="Skip launching Ramble")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
target = Path(target_str).expanduser().resolve()
|
||||||
|
target.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
say(f"[=] Installing Cascading Development into: {target}")
|
||||||
|
|
||||||
|
# Copy assets from installer into target
|
||||||
|
copy_install_assets_to_target(target)
|
||||||
|
|
||||||
|
# Ensure folder layout
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Create rules/templates and basic process docs
|
||||||
|
seed_process_and_rules(target)
|
||||||
|
|
||||||
|
# Initialize git & install pre-commit
|
||||||
|
ensure_git_repo(target)
|
||||||
|
install_precommit_hook(target)
|
||||||
|
|
||||||
|
# Launch Ramble (if available)
|
||||||
|
req = None
|
||||||
|
if not args.no_ramble:
|
||||||
|
req = run_ramble_and_collect(target, provider=args.provider)
|
||||||
|
|
||||||
|
# Seed first feature based on Ramble output
|
||||||
|
seed_initial_feature(target, req)
|
||||||
|
|
||||||
|
# First commit
|
||||||
|
first_commit(target)
|
||||||
|
|
||||||
|
say("[✓] Setup complete.")
|
||||||
|
say(f"Next steps:\n cd {target}\n git status")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Reference in New Issue