Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .flow/epics/fn-5.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"branch_name": "pr-22-user-flag",
"created_at": "2026-01-13T05:56:37.540724Z",
"depends_on_epics": [],
"id": "fn-5",
"next_task": 1,
"plan_review_status": "unknown",
"plan_reviewed_at": null,
"spec_path": ".flow/specs/fn-5.md",
"status": "open",
"title": "PR 22: User-level storage (--user flag)",
"updated_at": "2026-01-13T05:56:37.540740Z"
}
20 changes: 20 additions & 0 deletions .flow/specs/fn-5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# fn-5 PR 22: User-level storage (--user flag)

## Overview
TBD

## Scope
TBD

## Approach
TBD

## Quick commands
<!-- Required: at least one smoke command for the repo -->
- `# e.g., npm test, bun test, make test`

## Acceptance
- [ ] TBD

## References
- TBD
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ plans/
todos/
/docs/
!plugins/*/docs/
plugins/flow-next/scripts/__pycache__/
1 change: 1 addition & 0 deletions plugins/flow-next/VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.4.0
112 changes: 85 additions & 27 deletions plugins/flow-next/scripts/flowctl.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ def get_flow_dir() -> Path:
return get_repo_root() / FLOW_DIR


def get_user_config_dir() -> Path:
"""Get user-level config directory (~/.config/flow-next/)."""
return Path.home() / ".config" / "flow-next"


def ensure_flow_exists() -> bool:
"""Check if .flow/ exists."""
return get_flow_dir().exists()
Expand All @@ -76,17 +81,48 @@ def get_default_config() -> dict:
return {"memory": {"enabled": False}}


def load_flow_config() -> dict:
"""Load .flow/config.json, returning defaults if missing."""
config_path = get_flow_dir() / CONFIG_FILE
defaults = get_default_config()
def deep_merge(base: dict, override: dict) -> dict:
"""Deep merge two dicts. Override values take precedence."""
result = base.copy()
for key, value in override.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
result[key] = deep_merge(result[key], value)
else:
result[key] = value
return result


def load_user_config() -> dict:
"""Load user-level config from ~/.config/flow-next/config.json."""
config_path = get_user_config_dir() / CONFIG_FILE
if not config_path.exists():
return defaults
return {}
try:
data = json.loads(config_path.read_text(encoding="utf-8"))
return data if isinstance(data, dict) else defaults
return data if isinstance(data, dict) else {}
except (json.JSONDecodeError, Exception):
return defaults
return {}


def load_flow_config() -> dict:
"""Load config, merging user-level with project-level (project overrides user)."""
defaults = get_default_config()
user_config = load_user_config()

# Start with defaults, overlay user config
config = deep_merge(defaults, user_config)

# Overlay project config if .flow/ exists
config_path = get_flow_dir() / CONFIG_FILE
if config_path.exists():
try:
project_config = json.loads(config_path.read_text(encoding="utf-8"))
if isinstance(project_config, dict):
config = deep_merge(config, project_config)
except (json.JSONDecodeError, Exception):
pass

return config


def get_config(key: str, default=None):
Expand All @@ -101,16 +137,28 @@ def get_config(key: str, default=None):
return config if config != {} else default


def set_config(key: str, value) -> dict:
"""Set nested config value and return updated config."""
config_path = get_flow_dir() / CONFIG_FILE
def set_config(key: str, value, user_level: bool = False) -> dict:
"""Set nested config value and return updated config.

Args:
key: Config key (e.g., 'memory.enabled')
value: Value to set
user_level: If True, set in ~/.config/flow-next/config.json instead of .flow/config.json
"""
if user_level:
config_dir = get_user_config_dir()
config_dir.mkdir(parents=True, exist_ok=True)
config_path = config_dir / CONFIG_FILE
else:
config_path = get_flow_dir() / CONFIG_FILE

if config_path.exists():
try:
config = json.loads(config_path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, Exception):
config = get_default_config()
config = {}
else:
config = get_default_config()
config = {}

# Navigate/create nested path
parts = key.split(".")
Expand Down Expand Up @@ -1077,8 +1125,8 @@ def cmd_init(args: argparse.Namespace) -> None:
meta = {"schema_version": SCHEMA_VERSION, "next_epic": 1}
atomic_write_json(flow_dir / META_FILE, meta)

# Create config.json with defaults
atomic_write_json(flow_dir / CONFIG_FILE, get_default_config())
# Create empty config.json (inherits from user config)
atomic_write_json(flow_dir / CONFIG_FILE, {})

if args.json:
json_output({"message": ".flow/ initialized", "path": str(flow_dir)})
Expand Down Expand Up @@ -1135,12 +1183,8 @@ def cmd_detect(args: argparse.Namespace) -> None:


def cmd_config_get(args: argparse.Namespace) -> None:
"""Get a config value."""
if not ensure_flow_exists():
error_exit(
".flow/ does not exist. Run 'flowctl init' first.", use_json=args.json
)

"""Get a config value (merges user-level + project-level)."""
# Config get works even without .flow/ (reads user config)
value = get_config(args.key)
if args.json:
json_output({"key": args.key, "value": value})
Expand All @@ -1154,19 +1198,32 @@ def cmd_config_get(args: argparse.Namespace) -> None:


def cmd_config_set(args: argparse.Namespace) -> None:
"""Set a config value."""
if not ensure_flow_exists():
"""Set a config value (use --user for user-level config)."""
user_level = getattr(args, "user", False)

if not user_level and not ensure_flow_exists():
error_exit(
".flow/ does not exist. Run 'flowctl init' first.", use_json=args.json
".flow/ does not exist. Use --user for user-level config or run 'flowctl init' first.",
use_json=args.json,
)

set_config(args.key, args.value)
new_value = get_config(args.key)
set_config(args.key, args.value, user_level=user_level)

# Parse the value the same way set_config does for display
display_value = args.value
if isinstance(display_value, str):
if display_value.lower() == "true":
display_value = True
elif display_value.lower() == "false":
display_value = False
elif display_value.isdigit():
display_value = int(display_value)

location = "user-level (~/.config/flow-next/)" if user_level else "project (.flow/)"
if args.json:
json_output({"key": args.key, "value": new_value, "message": f"{args.key} set"})
json_output({"key": args.key, "value": display_value, "location": location, "message": f"{args.key} set"})
else:
print(f"{args.key} set to {new_value}")
print(f"{args.key} = {display_value} ({location})")


MEMORY_TEMPLATES = {
Expand Down Expand Up @@ -3617,6 +3674,7 @@ def main() -> None:
p_config_set = config_sub.add_parser("set", help="Set config value")
p_config_set.add_argument("key", help="Config key (e.g., memory.enabled)")
p_config_set.add_argument("value", help="Config value")
p_config_set.add_argument("--user", action="store_true", help="Set in user-level config (~/.config/flow-next/)")
p_config_set.add_argument("--json", action="store_true", help="JSON output")
p_config_set.set_defaults(func=cmd_config_set)

Expand Down
88 changes: 70 additions & 18 deletions plugins/flow-next/skills/flow-next-ralph-init/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,49 @@
---
name: flow-next-ralph-init
description: Scaffold repo-local Ralph autonomous harness under scripts/ralph/. Use when user runs /flow-next:ralph-init.
description: Scaffold Ralph autonomous harness. Supports project-local (scripts/ralph/) or user-level (~/.config/flow-next/ralph/) modes. Use when user runs /flow-next:ralph-init.
---

# Ralph init

Scaffold repo-local Ralph harness. Opt-in only.
Scaffold Ralph autonomous harness. Opt-in only. Safe to re-run for updates.

## Installation Modes

### Project-local (default)
- Everything in `scripts/ralph/` in the current repo
- Scripts, config, and runs all in one place
- Good for: single project, team sharing via git

### User-level (`--user` flag)
- Scripts in `~/.config/flow-next/ralph/` (shared across projects)
- User config in `~/.config/flow-next/ralph/config.env` (defaults)
- Project config in `scripts/ralph/config.env` (overrides)
- Runs stay in project `scripts/ralph/runs/`
- Good for: multiple projects, personal workflow, easy updates

## Rules

- Only create `scripts/ralph/` in the current repo.
- If `scripts/ralph/` already exists, stop and ask the user to remove it first.
- Copy templates from `templates/` into `scripts/ralph/`.
- Copy `flowctl` and `flowctl.py` from `${CLAUDE_PLUGIN_ROOT}/scripts/` into `scripts/ralph/`.
- Set executable bit on `scripts/ralph/ralph.sh`, `scripts/ralph/ralph_once.sh`, and `scripts/ralph/flowctl`.
### Project-local mode
- Create `scripts/ralph/` in the current repo
- If exists, ask user if they want to update or skip
- Copy all templates from `templates/` into `scripts/ralph/`
- Copy `flowctl` and `flowctl.py` from `${CLAUDE_PLUGIN_ROOT}/scripts/`
- Set executable bits

### User-level mode (`--user`)
- Create `~/.config/flow-next/ralph/` if not exists
- If exists, backup modified files then update from plugin
- Copy scripts to user dir: `ralph.sh`, `ralph_once.sh`, `flowctl`, `flowctl.py`, `watch-filter.py`, `prompt_*.md`, `config.env`
- Write VERSION file to track plugin version
- In project, create `scripts/ralph/` with:
- `config.env` (project-specific overrides)
- Symlinks: `ralph.sh -> ~/.config/flow-next/ralph/ralph.sh` etc.
- `runs/` directory for run logs

## Workflow

1. Resolve repo root: `git rev-parse --show-toplevel`
2. Check `scripts/ralph/` does not exist.
1. Parse arguments: check for `--user` flag
2. Resolve repo root: `git rev-parse --show-toplevel`
3. Detect available review backends:
```bash
HAVE_RP=$(which rp-cli >/dev/null 2>&1 && echo 1 || echo 0)
Expand All @@ -37,12 +62,39 @@ Scaffold repo-local Ralph harness. Opt-in only.
- If only rp-cli available: use `rp`
- If only codex available: use `codex`
- If neither available: use `none`
5. Write `scripts/ralph/config.env` with:
- `PLAN_REVIEW=<chosen>` and `WORK_REVIEW=<chosen>`
- replace `{{PLAN_REVIEW}}` and `{{WORK_REVIEW}}` placeholders in the template
6. Copy templates and flowctl files.
7. Print next steps (run from terminal, NOT inside Claude Code):
- Edit `scripts/ralph/config.env` to customize settings
- `./scripts/ralph/ralph_once.sh` (one iteration, observe)
- `./scripts/ralph/ralph.sh` (full loop, AFK)
- Uninstall: `rm -rf scripts/ralph/`

### If project-local mode (no --user):
5. Check `scripts/ralph/` - if exists, ask "Update existing? (y/n)"
6. Copy templates to `scripts/ralph/`
7. Copy flowctl files
8. Replace `{{PLAN_REVIEW}}` and `{{WORK_REVIEW}}` in config.env (only on first install)
9. Set executable bits

### If user-level mode (--user):
5. Check `~/.config/flow-next/ralph/VERSION` for existing version
6. If exists: backup modified files to `~/.config/flow-next/ralph/backups/<timestamp>/`
7. Copy scripts to user dir
8. Update VERSION file with plugin version
9. In project `scripts/ralph/`:
- Create config.env if not exists (project overrides only)
- Create/update symlinks to user scripts
- Create `runs/` directory
10. Set executable bits on user scripts

## Print next steps

### Project-local:
- Edit `scripts/ralph/config.env` to customize settings
- `./scripts/ralph/ralph_once.sh` (one iteration, observe)
- `./scripts/ralph/ralph.sh` (full loop, AFK)
- Update: re-run `/flow-next:ralph-init`
- Uninstall: `rm -rf scripts/ralph/`

### User-level:
- Edit `~/.config/flow-next/ralph/config.env` for user defaults
- Edit `scripts/ralph/config.env` for project-specific overrides
- `./scripts/ralph/ralph.sh` (runs from user-level scripts)
- Update: re-run `/flow-next:ralph-init --user` after plugin updates
- Backups: `~/.config/flow-next/ralph/backups/` (if files were modified)
- Uninstall project: `rm -rf scripts/ralph/`
- Uninstall user-level: `rm -rf ~/.config/flow-next/ralph/`
Loading