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 ## Participation
- Maintainer: Kickoff. VOTE: READY - 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 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("--prompt", default="Explain your new feature idea")
p.add_argument("--fields", nargs="+", default=["Summary","Title","Intent","ProblemItSolves","BriefOverview"]) p.add_argument("--fields", nargs="+", default=["Summary","Title","Intent","ProblemItSolves","BriefOverview"])
p.add_argument("--criteria", default="", help="JSON mapping of field -> criteria") p.add_argument("--criteria", default="", help="JSON mapping of field -> criteria")
p.add_argument("--hints", default="", help="JSON list of hint strings")
p.add_argument("--timeout", type=int, default=90) p.add_argument("--timeout", type=int, default=90)
p.add_argument("--tail", type=int, default=6000) p.add_argument("--tail", type=int, default=6000)
p.add_argument("--debug", action="store_true") p.add_argument("--debug", action="store_true")
@ -733,11 +734,23 @@ if __name__ == "__main__":
print("[FATAL] 'requests' is required for image backends. pip install requests", file=sys.stderr) print("[FATAL] 'requests' is required for image backends. pip install requests", file=sys.stderr)
sys.exit(3) sys.exit(3)
# Parse JSON args (tolerate empty/invalid)
try:
criteria = json.loads(args.criteria) if args.criteria else {}
if not isinstance(criteria, dict): criteria = {}
except Exception:
criteria = {}
try:
hints = json.loads(args.hints) if args.hints else None
if hints is not None and not isinstance(hints, list): hints = None
except Exception:
hints = None
demo = open_ramble_dialog( demo = open_ramble_dialog(
prompt=args.prompt, prompt=args.prompt,
fields=args.fields, fields=args.fields,
field_criteria=criteria, field_criteria=criteria,
hints=None, hints = hints,
provider=provider, provider=provider,
enable_stability=args.stability, enable_stability=args.stability,
enable_pexels=args.pexels, 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 Request: <title>
**Feature ID**: <FR_YYYY-MM-DD_slug> <!--META
**Intent**: <one paragraph describing purpose> {
**Motivation / Problem**: <why this is needed now> "kind": "feature_request",
**Constraints / Non-Goals**: <bulleted list of limitations> "ramble_fields": [
**Rough Proposal**: <short implementation outline> {"name": "Title", "hint": "camelCase, ≤24 chars", "default": "initialProjectDesign"},
**Open Questions**: <bulleted list of uncertainties> {"name": "Intent"},
**Meta**: Created: <date> • Author: <name> {"name": "ProblemItSolves"},
Discussion Template (process/templates/discussion.md): {"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: feature_request:
outputs: outputs:
feature_discussion: feature_discussion:
path: "{dir}/discussions/feature.discussion.md" path: "{dir}/discussions/feature.feature.discussion.md"
output_type: "feature_discussion_writer" output_type: "feature_discussion_writer"
instruction: | instruction: |
If missing: create with standard header (stage: feature, status: OPEN), If missing: create with standard header (stage: feature, status: OPEN),
@ -870,7 +870,7 @@ rules:
outputs: outputs:
# 1) Append the new AI comment to the discussion (append-only) # 1) Append the new AI comment to the discussion (append-only)
self_append: self_append:
path: "{dir}/discussions/feature.discussion.md" path: "{dir}/discussions/feature.feature.discussion.md"
output_type: "feature_discussion_writer" output_type: "feature_discussion_writer"
instruction: | instruction: |
Append concise comment signed with AI name, ending with a single vote line. Append concise comment signed with AI name, ending with a single vote line.
@ -890,7 +890,7 @@ rules:
# 3) Promotion artifacts when READY_FOR_DESIGN # 3) Promotion artifacts when READY_FOR_DESIGN
design_discussion: design_discussion:
path: "{dir}/discussions/design.discussion.md" path: "{dir}/discussions/design.feature.discussion.md"
output_type: "design_discussion_writer" output_type: "design_discussion_writer"
instruction: | instruction: |
Create ONLY if feature discussion status is READY_FOR_DESIGN. Create ONLY if feature discussion status is READY_FOR_DESIGN.
@ -930,7 +930,7 @@ rules:
Update only the marker-bounded sections from the discussion content. Update only the marker-bounded sections from the discussion content.
impl_discussion: impl_discussion:
path: "{dir}/discussions/implementation.discussion.md" path: "{dir}/discussions/implementation.feature.discussion.md"
output_type: "impl_discussion_writer" output_type: "impl_discussion_writer"
instruction: | instruction: |
Create ONLY if design discussion status is READY_FOR_IMPLEMENTATION. Create ONLY if design discussion status is READY_FOR_IMPLEMENTATION.
@ -974,7 +974,7 @@ rules:
Include unchecked items from ../implementation/tasks.md in ACTION_ITEMS. Include unchecked items from ../implementation/tasks.md in ACTION_ITEMS.
test_discussion: test_discussion:
path: "{dir}/discussions/testing.discussion.md" path: "{dir}/discussions/testing.feature.discussion.md"
output_type: "test_discussion_writer" output_type: "test_discussion_writer"
instruction: | instruction: |
Create ONLY if implementation status is READY_FOR_TESTING. Create ONLY if implementation status is READY_FOR_TESTING.
@ -1024,7 +1024,7 @@ rules:
Initialize bug discussion and fix plan in the same folder. Initialize bug discussion and fix plan in the same folder.
review_discussion: review_discussion:
path: "{dir}/discussions/review.discussion.md" path: "{dir}/discussions/review.feature.discussion.md"
output_type: "review_discussion_writer" output_type: "review_discussion_writer"
instruction: | instruction: |
Create ONLY if all test checklist items pass. Create ONLY if all test checklist items pass.
@ -1141,7 +1141,7 @@ resolve_template() {
ext="${basename##*.}" ext="${basename##*.}"
# nearest FR_* ancestor as feature_id # nearest FR_* ancestor as feature_id
feature_id="$(echo "$rel_path" | sed -n 's|.*Docs/features/\(FR_[^/]*\).*|\1|p')" feature_id="$(echo "$rel_path" | sed -n 's|.*Docs/features/\(FR_[^/]*\).*|\1|p')"
# infer stage from <stage>.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')" stage="$(echo "$basename" | sed -n 's/^\([A-Za-z0-9_-]\+\)\.discussion\.md$/\1/p')"
echo "$tmpl" \ echo "$tmpl" \
| sed -e "s|{date}|$today|g" \ | sed -e "s|{date}|$today|g" \
@ -1288,7 +1288,7 @@ Rule Definition (in Docs/features/.ai-rules.yml):
discussion_moderator_nudge: discussion_moderator_nudge:
outputs: outputs:
self_append: self_append:
path: "{dir}/discussions/{stage}.discussion.md" path: "{dir}/discussions/{stage}.feature.discussion.md"
output_type: "discussion_moderator_writer" output_type: "discussion_moderator_writer"
instruction: | instruction: |
Act as AI_Moderator. Analyze the entire discussion and: Act as AI_Moderator. Analyze the entire discussion and:
@ -1425,7 +1425,7 @@ Bypass & Minimal Patch:
```bash ```bash
.git/ai-rules-debug/ .git/ai-rules-debug/
├─ 20251021-143022-12345-feature.discussion.md/ ├─ 20251021-143022-12345-feature.feature.discussion.md/
│ ├─ raw.out # Raw model output │ ├─ raw.out # Raw model output
│ ├─ clean.diff # Extracted patch │ ├─ clean.diff # Extracted patch
│ ├─ sanitized.diff # After sanitization │ ├─ sanitized.diff # After sanitization
@ -1975,7 +1975,7 @@ Docs/features/FR_.../
type: discussion-summary type: discussion-summary
stage: feature # feature|design|implementation|testing|review stage: feature # feature|design|implementation|testing|review
status: ACTIVE # ACTIVE|SNAPSHOT|ARCHIVED status: ACTIVE # ACTIVE|SNAPSHOT|ARCHIVED
source_discussion: feature.discussion.md source_discussion: feature.feature.discussion.md
feature_id: FR_YYYY-MM-DD_<slug> feature_id: FR_YYYY-MM-DD_<slug>
updated: YYYY-MM-DDTHH:MM:SSZ updated: YYYY-MM-DDTHH:MM:SSZ
policy: policy:

View File

@ -13,7 +13,7 @@ Examples:
python setup_cascadingdev.py --target ~/dev/my-new-repo python setup_cascadingdev.py --target ~/dev/my-new-repo
python setup_cascadingdev.py --target /abs/path --no-ramble python setup_cascadingdev.py --target /abs/path --no-ramble
""" """
import json import json, re
import argparse import argparse
import datetime import datetime
import sys 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) proc = subprocess.Popen(cmd, cwd=cwd, stdout=sys.stdout, stderr=sys.stderr)
return proc.wait() return proc.wait()
# --- Tiny template helpers ----------------------------------------------------
# Self-contained; no external dependencies
_META_RE = re.compile(r"<!--META\s*(\{.*?\})\s*-->", re.S)
def load_template_with_meta(path: Path) -> tuple[dict, str]:
"""
Returns (meta: dict, body_without_meta: str). If no META, ({} , full text).
META must be a single JSON object inside <!--META ... -->.
"""
if not path.exists():
return {}, ""
text = path.read_text(encoding="utf-8")
m = _META_RE.search(text)
if not m:
return {}, text
meta_json = m.group(1)
try:
meta = json.loads(meta_json)
except Exception:
meta = {}
body = _META_RE.sub("", text, count=1).lstrip()
return meta, body
def render_placeholders(body: str, values: dict) -> str:
"""
Simple {Token} replacement. Leaves unknown tokens as-is.
"""
# two-pass: {{Token}} then {Token}
out = body
for k, v in values.items():
out = out.replace("{{" + k + "}}", str(v))
try:
out = out.format_map({k: v for k, v in values.items()})
except Exception:
pass
return out
def meta_ramble_config(meta: dict) -> tuple[list[str], dict, dict, list[str]]:
"""
From template META, extract:
- fields: list of field names in order
- defaults: {field: default_value}
- criteria: {field: rule/description} (optional)
- hints: [str, ...] (optional)
"""
fields: list[str] = []
defaults: dict = {}
for spec in meta.get("ramble_fields", []):
name = spec.get("name")
if name:
fields.append(name)
if "default" in spec:
defaults[name] = spec["default"]
criteria = meta.get("criteria", {}) or {}
hints = meta.get("hints", []) or []
return fields, defaults, criteria, hints
def ensure_git_repo(target: Path): def ensure_git_repo(target: Path):
"""Initialize a git repository if one doesn't exist at the target path.""" """Initialize a git repository if one doesn't exist at the target path."""
if not (target / ".git").exists(): if not (target / ".git").exists():
# Initialize git repo with main branch # Initialize git repo with main branch
run(["git", "init", "-b", "main"], cwd=target) run(["git", "init", "-b", "main"], cwd=target)
# Create basic .gitignore file # Seed .gitignore from template if present; otherwise fallback
write_if_missing(target / ".gitignore", "\n".join([ tmpl_gitignore = INSTALL_ROOT / "assets" / "templates" / "root_gitignore"
".env", ".env.*", "secrets/", ".git/ai-rules-*", "__pycache__/", if tmpl_gitignore.exists():
"*.pyc", ".pytest_cache/", ".DS_Store", copy_if_missing(tmpl_gitignore, target / ".gitignore")
]) + "\n") else:
write_if_missing(target / ".gitignore", "\n".join([
"__pycache__/", "*.py[cod]", "*.egg-info/", ".pytest_cache/",
".mypy_cache/", ".coverage", "htmlcov/", "node_modules/",
"dist/", "build/", ".env", ".env.*", "secrets/", ".DS_Store",
".git/ai-rules-*",
]) + "\n")
def install_precommit_hook(target: Path): def install_precommit_hook(target: Path):
"""Install the pre-commit hook from installer assets to target git hooks.""" """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. Launch Ramble GUI to collect initial feature request details.
Falls back to terminal prompts if GUI fails or returns invalid JSON. Falls back to terminal prompts if GUI fails or returns invalid JSON.
""" """
# Find FR template + read META (for field names)
fr_tmpl = INSTALL_ROOT / "assets" / "templates" / "feature_request.md"
meta, _ = load_template_with_meta(fr_tmpl)
field_names, _defaults, criteria, hints = meta_ramble_config(meta)
# Fallback to your previous default fields if template lacks META
if not field_names:
field_names = ["Summary", "Title", "Intent", "ProblemItSolves", "BriefOverview"]
ramble = target / "ramble.py" ramble = target / "ramble.py"
if not ramble.exists(): if not ramble.exists():
say("[-] ramble.py not found in target; skipping interactive FR capture.") say("[-] ramble.py not found in target; skipping interactive FR capture.")
return None return None
# Build Ramble command arguments # Build Ramble arguments dynamically from the template-defined fields
args = [ args = [
sys.executable, str(ramble), sys.executable, str(ramble),
"--provider", provider, "--provider", provider,
"--claude-cmd", claude_cmd, "--claude-cmd", claude_cmd,
"--prompt", "Describe your initial feature request for this repository", "--prompt", "Describe your initial feature request for this repository",
"--fields", "Summary", "Title", "Intent", "ProblemItSolves", "BriefOverview", "--fields", *field_names,
"--criteria", '{"Summary":"<= 2 sentences","Title":"camelCase, <= 24 chars"}'
] ]
if criteria:
args += ["--criteria", json.dumps(criteria)]
if hints:
args += ["--hints", json.dumps(hints)]
say("[•] Launching Ramble (close the dialog with Submit to return JSON)…") say("[•] Launching Ramble (close the dialog with Submit to return JSON)…")
proc = subprocess.run(args, text=True, capture_output=True, cwd=str(target)) proc = subprocess.run(args, text=True, capture_output=True, cwd=str(target))
@ -170,53 +246,72 @@ def seed_process_and_rules(target: Path):
copy_if_missing(t_rules_features, rules_dir / ".ai-rules.yml") copy_if_missing(t_rules_features, rules_dir / ".ai-rules.yml")
def seed_initial_feature(target: Path, req_fields: dict | None): def seed_initial_feature(target: Path, req_fields: dict | None):
"""Create the initial feature request and associated discussion files."""
today = datetime.date.today().isoformat() 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 = fr_dir / "discussions"
disc_dir.mkdir(parents=True, exist_ok=True) disc_dir.mkdir(parents=True, exist_ok=True)
# Create feature request content, using Ramble data if available # Gather values from Ramble result (if any)
if req_fields: fields = (req_fields or {}).get("fields", {}) if req_fields else {}
title = (req_fields.get("fields", {}) or {}).get("Title", "").strip() or "initialProjectDesign" # Load FR template + META
intent = (req_fields.get("fields", {}) or {}).get("Intent", "").strip() or "" fr_tmpl = INSTALL_ROOT / "assets" / "templates" / "feature_request.md"
problem = (req_fields.get("fields", {}) or {}).get("ProblemItSolves", "").strip() or "" fr_meta, fr_body = load_template_with_meta(fr_tmpl)
brief = (req_fields.get("fields", {}) or {}).get("BriefOverview", "").strip() or "" field_names, defaults, _criteria, _hints = meta_ramble_config(fr_meta)
summary = (req_fields.get("summary") or "").strip()
body = f"""# Feature Request: {title} # Build values map with defaults → ramble fields → system tokens
values = {}
values.update(defaults) # template defaults
values.update(fields) # user-entered
values.update({ # system tokens
"FeatureId": feature_id,
"CreatedDate": today,
})
# If no template body, fall back to your old default
if not fr_body.strip():
title = values.get("Title", "initialProjectDesign")
intent = values.get("Intent", "")
problem = values.get("ProblemItSolves", "")
brief = values.get("BriefOverview", "")
summary = values.get("Summary", "")
fr_body = f"""# Feature Request: {title}
**Intent**: {intent} **Intent**: {intent}
**Motivation / Problem**: {problem} **Motivation / Problem**: {problem}
**Brief Overview**: {brief} **Brief Overview**: {brief}
**Summary**: {summary} **Summary**: {summary}
**Meta**: Created: {today} **Meta**: Created: {today}
""" """
(fr_dir / "request.md").write_text(render_placeholders(fr_body, values), encoding="utf-8")
# --- feature.discussion.md ---
disc_tmpl = INSTALL_ROOT / "assets" / "templates" / "feature.discussion.md"
d_meta, d_body = load_template_with_meta(disc_tmpl)
# Always include the front-matter for rules, then template body (or fallback)
fm = f"""---\ntype: discussion\nstage: feature\nstatus: OPEN\nfeature_id: {feature_id}\ncreated: {today}\n---\n"""
if not d_body.strip():
d_body = (
"## Summary\n"
f"Initial discussion for {feature_id}. Append your comments below.\n\n"
"## Participation\n"
"- Maintainer: Kickoff. VOTE: READY\n"
)
(disc_dir / "feature.discussion.md").write_text(fm + render_placeholders(d_body, values), encoding="utf-8")
# --- feature.discussion.sum.md ---
sum_tmpl = INSTALL_ROOT / "assets" / "templates" / "feature.discussion.sum.md"
s_meta, s_body = load_template_with_meta(sum_tmpl)
if s_body.strip():
# use template
(disc_dir / "feature.discussion.sum.md").write_text(render_placeholders(s_body, values), encoding="utf-8")
else: else:
# Fallback to template content if no Ramble data # your existing static content
body = (target / "process" / "templates" / "feature_request.md").read_text(encoding="utf-8") (disc_dir / "feature.discussion.sum.md").write_text(
"""# Summary — Feature
(fr_dir / "request.md").write_text(body, encoding="utf-8")
# 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
<!-- SUMMARY:DECISIONS START --> <!-- SUMMARY:DECISIONS START -->
## Decisions (ADR-style) ## Decisions (ADR-style)

View File

@ -22,7 +22,15 @@ def main():
shutil.copy2(ROOT / "assets" / "hooks" / "pre-commit", BUNDLE / "assets" / "hooks" / "pre-commit") shutil.copy2(ROOT / "assets" / "hooks" / "pre-commit", BUNDLE / "assets" / "hooks" / "pre-commit")
# copy core templates # 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) shutil.copy2(ROOT / "assets" / "templates" / t, BUNDLE / "assets" / "templates" / t)
# copy (recursively) the contents of process/ and rules/ templates folders # copy (recursively) the contents of process/ and rules/ templates folders

View File

@ -6,7 +6,7 @@ def main():
required = [ required = [
root / "assets" / "hooks" / "pre-commit", root / "assets" / "hooks" / "pre-commit",
root / "assets" / "templates" / "feature_request.md", 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" / "design_doc.md",
root / "assets" / "templates" / "USER_GUIDE.md", # now required root / "assets" / "templates" / "USER_GUIDE.md", # now required
root / "assets" / "runtime" / "ramble.py", root / "assets" / "runtime" / "ramble.py",