ralph-any 0.1.1__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ralph/__init__.py CHANGED
@@ -1,12 +1,14 @@
1
1
  """Ralph Any — iterative AI dev loop via ACP."""
2
2
 
3
- __version__ = "0.1.1"
3
+ __version__ = "0.2.0"
4
4
 
5
+ from ralph.config import load_config_file
5
6
  from ralph.detect import detect_promise
6
7
  from ralph.engine import LoopConfig, LoopResult, RalphEngine
7
8
 
8
9
  __all__ = [
9
10
  "__version__",
11
+ "load_config_file",
10
12
  "detect_promise",
11
13
  "LoopConfig",
12
14
  "LoopResult",
ralph/cli.py CHANGED
@@ -7,7 +7,9 @@ import asyncio
7
7
  import shlex
8
8
  import sys
9
9
  from pathlib import Path
10
+ from typing import Any
10
11
 
12
+ from ralph.config import load_config_file
11
13
  from ralph.engine import LoopConfig, RalphEngine
12
14
 
13
15
  EXIT_SUCCESS = 0
@@ -24,6 +26,9 @@ _STATE_TO_EXIT = {
24
26
  "max_iterations": EXIT_MAX_ITERATIONS,
25
27
  }
26
28
 
29
+ # Auto-detected prompt files, checked in order.
30
+ _AUTO_PROMPT_FILES = ("ralph.md", "TASK.md", "ralph.txt", "TASK.txt")
31
+
27
32
 
28
33
  def _build_parser() -> argparse.ArgumentParser:
29
34
  p = argparse.ArgumentParser(
@@ -32,38 +37,40 @@ def _build_parser() -> argparse.ArgumentParser:
32
37
  )
33
38
  p.add_argument(
34
39
  "prompt",
35
- help="Task description, or path to a .md/.txt file",
40
+ nargs="?",
41
+ default=None,
42
+ help="Task description, or path to a .md/.txt file (auto-detects ralph.md / TASK.md)",
36
43
  )
37
44
  p.add_argument(
38
45
  "-m", "--max-iterations",
39
46
  type=int,
40
- default=10,
47
+ default=None,
41
48
  help="Maximum loop iterations (default: 10)",
42
49
  )
43
50
  p.add_argument(
44
51
  "-t", "--timeout",
45
52
  type=int,
46
- default=1800,
53
+ default=None,
47
54
  help="Maximum runtime in seconds (default: 1800 = 30m)",
48
55
  )
49
56
  p.add_argument(
50
57
  "--promise",
51
- default="任務完成!🥇",
58
+ default=None,
52
59
  help="Completion promise phrase (default: 任務完成!🥇)",
53
60
  )
54
61
  p.add_argument(
55
62
  "-c", "--command",
56
- default="claude-code-acp",
63
+ default=None,
57
64
  help="ACP CLI command (default: claude-code-acp)",
58
65
  )
59
66
  p.add_argument(
60
67
  "--command-args",
61
- default="",
68
+ default=None,
62
69
  help="Extra arguments for the ACP CLI (e.g. '--experimental-acp')",
63
70
  )
64
71
  p.add_argument(
65
72
  "-d", "--working-dir",
66
- default=".",
73
+ default=None,
67
74
  help="Working directory (default: .)",
68
75
  )
69
76
  p.add_argument(
@@ -82,6 +89,17 @@ def _resolve_prompt(raw: str) -> str:
82
89
  return raw
83
90
 
84
91
 
92
+ def _auto_detect_prompt(working_dir: str) -> str | None:
93
+ """Look for a default prompt file in the working directory."""
94
+ base = Path(working_dir)
95
+ for name in _AUTO_PROMPT_FILES:
96
+ p = base / name
97
+ if p.is_file():
98
+ print(f"📄 Auto-detected prompt file: {p}", flush=True)
99
+ return p.read_text(encoding="utf-8")
100
+ return None
101
+
102
+
85
103
  def _run(config: LoopConfig) -> int:
86
104
  engine = RalphEngine(config)
87
105
  try:
@@ -100,18 +118,49 @@ def main(argv: list[str] | None = None) -> None:
100
118
  parser = _build_parser()
101
119
  args = parser.parse_args(argv)
102
120
 
103
- prompt_text = _resolve_prompt(args.prompt)
104
-
105
- config = LoopConfig(
106
- prompt=prompt_text,
107
- promise_phrase=args.promise,
108
- command=args.command,
109
- command_args=shlex.split(args.command_args) if args.command_args else [],
110
- working_dir=args.working_dir,
111
- max_iterations=args.max_iterations,
112
- timeout_seconds=args.timeout,
113
- dry_run=args.dry_run,
114
- )
121
+ # Layer 1: defaults
122
+ cfg: dict[str, Any] = {
123
+ "prompt": None,
124
+ "promise_phrase": "任務完成!🥇",
125
+ "command": "claude-code-acp",
126
+ "command_args": [],
127
+ "working_dir": ".",
128
+ "max_iterations": 10,
129
+ "timeout_seconds": 1800,
130
+ "dry_run": False,
131
+ }
132
+
133
+ # Layer 2: ralph.yml (overrides defaults)
134
+ file_cfg = load_config_file(args.working_dir or ".")
135
+ if file_cfg:
136
+ cfg.update({k: v for k, v in file_cfg.items() if v is not None})
137
+
138
+ # Layer 3: CLI args (overrides config file)
139
+ if args.prompt is not None:
140
+ cfg["prompt"] = _resolve_prompt(args.prompt)
141
+ if args.max_iterations is not None:
142
+ cfg["max_iterations"] = args.max_iterations
143
+ if args.timeout is not None:
144
+ cfg["timeout_seconds"] = args.timeout
145
+ if args.promise is not None:
146
+ cfg["promise_phrase"] = args.promise
147
+ if args.command is not None:
148
+ cfg["command"] = args.command
149
+ if args.command_args is not None:
150
+ cfg["command_args"] = shlex.split(args.command_args)
151
+ if args.working_dir is not None:
152
+ cfg["working_dir"] = args.working_dir
153
+ if args.dry_run:
154
+ cfg["dry_run"] = True
155
+
156
+ # Auto-detect prompt file if no prompt given
157
+ if cfg["prompt"] is None:
158
+ cfg["prompt"] = _auto_detect_prompt(cfg["working_dir"])
159
+
160
+ if cfg["prompt"] is None:
161
+ parser.error("prompt is required (provide as argument or create ralph.md / TASK.md)")
162
+
163
+ config = LoopConfig(**cfg)
115
164
 
116
165
  if config.dry_run:
117
166
  print("Dry-run config:")
ralph/config.py ADDED
@@ -0,0 +1,55 @@
1
+ """Load ralph.yml config file."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shlex
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+
10
+ def load_config_file(working_dir: str) -> dict[str, Any] | None:
11
+ """Load ralph.yml / ralph.yaml from *working_dir*. Returns None if absent."""
12
+ base = Path(working_dir)
13
+ for name in ("ralph.yml", "ralph.yaml"):
14
+ path = base / name
15
+ if path.is_file():
16
+ return _parse(path)
17
+ return None
18
+
19
+
20
+ def _parse(path: Path) -> dict[str, Any]:
21
+ """Parse a ralph config file (simple YAML subset, no dependency needed)."""
22
+ raw: dict[str, str] = {}
23
+ for line in path.read_text(encoding="utf-8").splitlines():
24
+ line = line.strip()
25
+ if not line or line.startswith("#"):
26
+ continue
27
+ if ":" not in line:
28
+ continue
29
+ key, _, value = line.partition(":")
30
+ raw[key.strip()] = value.strip()
31
+
32
+ cfg: dict[str, Any] = {}
33
+
34
+ _map = {
35
+ "command": "command",
36
+ "command_args": "command_args",
37
+ "promise": "promise_phrase",
38
+ "max_iterations": "max_iterations",
39
+ "timeout": "timeout_seconds",
40
+ "working_dir": "working_dir",
41
+ "prompt": "prompt",
42
+ }
43
+
44
+ for yaml_key, cfg_key in _map.items():
45
+ if yaml_key not in raw:
46
+ continue
47
+ val = raw[yaml_key]
48
+ if cfg_key in ("max_iterations", "timeout_seconds"):
49
+ cfg[cfg_key] = int(val)
50
+ elif cfg_key == "command_args":
51
+ cfg[cfg_key] = shlex.split(val)
52
+ else:
53
+ cfg[cfg_key] = val
54
+
55
+ return cfg
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ralph-any
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
4
  Summary: Ralph Wiggum Loop — iterative AI dev loop via ACP (supports Claude, Gemini, and more)
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -0,0 +1,12 @@
1
+ ralph/__init__.py,sha256=fBtiHYjMPjo4atoDtuKfSI_efE7ai1nSPLDheW-8nFY,353
2
+ ralph/__main__.py,sha256=FdEO4lE2dw6-b4Kk97vznatvCAEuO-6qFwOkWP6en-c,69
3
+ ralph/cli.py,sha256=HrM3H4HUdkh_0wmqAcXSVMucI90cJ2JVR4gWt5YJvco,4877
4
+ ralph/config.py,sha256=H7jvtUwV9lBf3lo5AZg4AkJr3zMlK_C93ZXNElFuJcg,1573
5
+ ralph/detect.py,sha256=e99V2o9KECN2fR6sZ7qLghVpdomShCW1Jf_rHjo0og0,180
6
+ ralph/engine.py,sha256=CZFU7GtmKnPIXuN9mx-5iSxcUCyCgUNZkilj1POuTyM,4600
7
+ ralph/prompt.py,sha256=9B5z6zeEyX0Jr6tJ6P7_ahMUVckQySWST51DypGdIDQ,2022
8
+ ralph_any-0.2.0.dist-info/METADATA,sha256=MYi1ivYBwXk_yyYD_7xn40B9KeXvqoo5Hs2oWopfVUA,350
9
+ ralph_any-0.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
10
+ ralph_any-0.2.0.dist-info/entry_points.txt,sha256=39mDVcb7RNWfrI1JwWs7ae3TrLLzwzy6R6EurlGgTfI,41
11
+ ralph_any-0.2.0.dist-info/licenses/LICENSE,sha256=Losvcv3YtvA9lWEq2Ngj6bbejg6sBzR5vxZKb-Db5i0,1079
12
+ ralph_any-0.2.0.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- ralph/__init__.py,sha256=MhQ-PrDvbQk55q3i2yxFjFMR8rkJYFTTnz9bNpMORlA,287
2
- ralph/__main__.py,sha256=FdEO4lE2dw6-b4Kk97vznatvCAEuO-6qFwOkWP6en-c,69
3
- ralph/cli.py,sha256=1uxhTV8U2sw_p9qOM26KekNI5dh3lpLl_lcBpw6BSls,3179
4
- ralph/detect.py,sha256=e99V2o9KECN2fR6sZ7qLghVpdomShCW1Jf_rHjo0og0,180
5
- ralph/engine.py,sha256=CZFU7GtmKnPIXuN9mx-5iSxcUCyCgUNZkilj1POuTyM,4600
6
- ralph/prompt.py,sha256=9B5z6zeEyX0Jr6tJ6P7_ahMUVckQySWST51DypGdIDQ,2022
7
- ralph_any-0.1.1.dist-info/METADATA,sha256=Wvs53pyw1B3BOiwHfcT6KcFgP4mmyAMWYqfeWjcr7jU,350
8
- ralph_any-0.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
9
- ralph_any-0.1.1.dist-info/entry_points.txt,sha256=39mDVcb7RNWfrI1JwWs7ae3TrLLzwzy6R6EurlGgTfI,41
10
- ralph_any-0.1.1.dist-info/licenses/LICENSE,sha256=Losvcv3YtvA9lWEq2Ngj6bbejg6sBzR5vxZKb-Db5i0,1079
11
- ralph_any-0.1.1.dist-info/RECORD,,