fluxloop-cli 0.1.0__tar.gz → 0.2.1__tar.gz

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.

Potentially problematic release.


This version of fluxloop-cli might be problematic. Click here for more details.

Files changed (39) hide show
  1. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/PKG-INFO +31 -27
  2. fluxloop_cli-0.2.1/README.md +50 -0
  3. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/fluxloop_cli/__init__.py +1 -1
  4. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/fluxloop_cli/commands/__init__.py +2 -2
  5. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/fluxloop_cli/commands/config.py +12 -2
  6. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/fluxloop_cli/commands/generate.py +4 -84
  7. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/fluxloop_cli/commands/init.py +39 -12
  8. fluxloop_cli-0.2.1/fluxloop_cli/commands/record.py +150 -0
  9. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/fluxloop_cli/commands/run.py +5 -1
  10. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/fluxloop_cli/commands/status.py +41 -5
  11. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/fluxloop_cli/config_loader.py +103 -14
  12. fluxloop_cli-0.2.1/fluxloop_cli/config_schema.py +83 -0
  13. fluxloop_cli-0.2.1/fluxloop_cli/constants.py +27 -0
  14. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/fluxloop_cli/main.py +2 -1
  15. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/fluxloop_cli/project_paths.py +60 -8
  16. fluxloop_cli-0.2.1/fluxloop_cli/templates.py +447 -0
  17. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/fluxloop_cli.egg-info/PKG-INFO +31 -27
  18. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/fluxloop_cli.egg-info/SOURCES.txt +2 -1
  19. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/pyproject.toml +1 -1
  20. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/tests/test_config_command.py +39 -15
  21. fluxloop_cli-0.1.0/README.md +0 -46
  22. fluxloop_cli-0.1.0/fluxloop_cli/constants.py +0 -12
  23. fluxloop_cli-0.1.0/fluxloop_cli/templates.py +0 -277
  24. fluxloop_cli-0.1.0/tests/test_generate_from_recording.py +0 -85
  25. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/fluxloop_cli/arg_binder.py +0 -0
  26. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/fluxloop_cli/commands/parse.py +0 -0
  27. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/fluxloop_cli/input_generator.py +0 -0
  28. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/fluxloop_cli/llm_generator.py +0 -0
  29. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/fluxloop_cli/runner.py +0 -0
  30. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/fluxloop_cli/target_loader.py +0 -0
  31. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/fluxloop_cli/validators.py +0 -0
  32. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/fluxloop_cli.egg-info/dependency_links.txt +0 -0
  33. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/fluxloop_cli.egg-info/entry_points.txt +0 -0
  34. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/fluxloop_cli.egg-info/requires.txt +0 -0
  35. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/fluxloop_cli.egg-info/top_level.txt +0 -0
  36. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/setup.cfg +0 -0
  37. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/tests/test_arg_binder.py +0 -0
  38. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/tests/test_input_generator.py +0 -0
  39. {fluxloop_cli-0.1.0 → fluxloop_cli-0.2.1}/tests/test_target_loader.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fluxloop-cli
3
- Version: 0.1.0
3
+ Version: 0.2.1
4
4
  Summary: FluxLoop CLI for running agent simulations
5
5
  Author-email: FluxLoop Team <team@fluxloop.dev>
6
6
  License: Apache-2.0
@@ -40,47 +40,51 @@ Requires-Dist: anthropic>=0.7.0; extra == "anthropic"
40
40
 
41
41
  # FluxLoop CLI
42
42
 
43
- Command-line interface for running agent simulations and managing FluxLoop workflows.
43
+ Command-line interface for running agent simulations.
44
44
 
45
45
  ## Installation
46
46
 
47
- ```bash
47
+ ```
48
48
  pip install fluxloop-cli
49
49
  ```
50
50
 
51
- ## Quick Start
52
-
53
- ```bash
54
- # Initialize a new FluxLoop project
55
- fluxloop init
51
+ ## Configuration Overview (v0.2.0)
56
52
 
57
- # Run agent simulations
58
- fluxloop run
53
+ FluxLoop CLI now stores experiment settings in four files under `configs/`:
59
54
 
60
- # Generate test inputs
61
- fluxloop generate
55
+ - `configs/project.yaml` project metadata, collector defaults
56
+ - `configs/input.yaml` – personas, base inputs, input generation options
57
+ - `configs/simulation.yaml` – runtime parameters (iterations, runner, replay args)
58
+ - `configs/evaluation.yaml` – evaluator definitions (rule-based, LLM judge, etc.)
62
59
 
63
- # Check status
64
- fluxloop status
65
- ```
60
+ The legacy `setting.yaml` is still supported, but new projects created with
61
+ `fluxloop init project` will generate the structured layout above.
66
62
 
67
- ## Features
63
+ ## Key Commands
68
64
 
69
- - 🚀 **Easy Setup**: Initialize projects with a single command
70
- - 🔄 **Simulation Runner**: Execute agent tests with various input scenarios
71
- - 📝 **Input Generation**: LLM-powered test input generation
72
- - 📊 **Rich Output**: Beautiful terminal UI with detailed progress tracking
65
+ - `fluxloop init project` scaffold a new project (configs, `.env`, examples)
66
+ - `fluxloop generate inputs` produce input variations for the active project
67
+ - `fluxloop run experiment` execute an experiment using `configs/simulation.yaml`
68
+ - `fluxloop parse experiment` convert experiment outputs into readable artifacts
69
+ - `fluxloop config set-llm` – update LLM provider/model in `configs/input.yaml`
70
+ - `fluxloop record enable|disable|status` – toggle recording mode across `.env` and simulation config
73
71
 
74
- ## Requirements
72
+ Run `fluxloop --help` or `fluxloop <command> --help` for more detail.
75
73
 
76
- - Python 3.8 or higher
77
- - FluxLoop SDK
74
+ ## Developing
78
75
 
79
- ## Documentation
76
+ Install dependencies and run tests:
80
77
 
81
- For detailed documentation, visit [https://docs.fluxloop.dev](https://docs.fluxloop.dev)
78
+ ```
79
+ python -m venv .venv
80
+ source .venv/bin/activate
81
+ pip install -e .[dev]
82
+ pytest
83
+ ```
82
84
 
83
- ## License
85
+ To package the CLI:
84
86
 
85
- Apache License 2.0 - see LICENSE file for details
87
+ ```
88
+ ./build.sh
89
+ ```
86
90
 
@@ -0,0 +1,50 @@
1
+ # FluxLoop CLI
2
+
3
+ Command-line interface for running agent simulations.
4
+
5
+ ## Installation
6
+
7
+ ```
8
+ pip install fluxloop-cli
9
+ ```
10
+
11
+ ## Configuration Overview (v0.2.0)
12
+
13
+ FluxLoop CLI now stores experiment settings in four files under `configs/`:
14
+
15
+ - `configs/project.yaml` – project metadata, collector defaults
16
+ - `configs/input.yaml` – personas, base inputs, input generation options
17
+ - `configs/simulation.yaml` – runtime parameters (iterations, runner, replay args)
18
+ - `configs/evaluation.yaml` – evaluator definitions (rule-based, LLM judge, etc.)
19
+
20
+ The legacy `setting.yaml` is still supported, but new projects created with
21
+ `fluxloop init project` will generate the structured layout above.
22
+
23
+ ## Key Commands
24
+
25
+ - `fluxloop init project` – scaffold a new project (configs, `.env`, examples)
26
+ - `fluxloop generate inputs` – produce input variations for the active project
27
+ - `fluxloop run experiment` – execute an experiment using `configs/simulation.yaml`
28
+ - `fluxloop parse experiment` – convert experiment outputs into readable artifacts
29
+ - `fluxloop config set-llm` – update LLM provider/model in `configs/input.yaml`
30
+ - `fluxloop record enable|disable|status` – toggle recording mode across `.env` and simulation config
31
+
32
+ Run `fluxloop --help` or `fluxloop <command> --help` for more detail.
33
+
34
+ ## Developing
35
+
36
+ Install dependencies and run tests:
37
+
38
+ ```
39
+ python -m venv .venv
40
+ source .venv/bin/activate
41
+ pip install -e .[dev]
42
+ pytest
43
+ ```
44
+
45
+ To package the CLI:
46
+
47
+ ```
48
+ ./build.sh
49
+ ```
50
+
@@ -2,7 +2,7 @@
2
2
  FluxLoop CLI - Command-line interface for running agent simulations.
3
3
  """
4
4
 
5
- __version__ = "0.1.0"
5
+ __version__ = "0.2.1"
6
6
 
7
7
  from .main import app
8
8
 
@@ -1,5 +1,5 @@
1
1
  """CLI commands."""
2
2
 
3
- from . import config, generate, init, parse, run, status
3
+ from . import config, generate, init, parse, run, status, record
4
4
 
5
- __all__ = ["config", "generate", "init", "parse", "run", "status"]
5
+ __all__ = ["config", "generate", "init", "parse", "run", "status", "record"]
@@ -15,6 +15,7 @@ from rich.table import Table
15
15
  from ..config_loader import load_experiment_config
16
16
  from ..templates import create_env_file, create_gitignore, create_sample_agent
17
17
  from ..constants import DEFAULT_CONFIG_PATH, DEFAULT_ROOT_DIR_NAME
18
+ from ..config_schema import CONFIG_SECTION_FILENAMES
18
19
  from ..project_paths import (
19
20
  resolve_config_path,
20
21
  resolve_env_path,
@@ -228,7 +229,12 @@ def set_llm(
228
229
  api_key: str = typer.Argument(..., help="API key or token for the provider"),
229
230
  model: Optional[str] = typer.Option(None, "--model", "-m", help="Default model to use"),
230
231
  overwrite_env: bool = typer.Option(False, "--overwrite-env", help="Overwrite existing key in .env"),
231
- config_file: Path = typer.Option(DEFAULT_CONFIG_PATH, "--file", "-f", help="Configuration file to update"),
232
+ config_file: Path = typer.Option(
233
+ Path(CONFIG_SECTION_FILENAMES["input"]),
234
+ "--file",
235
+ "-f",
236
+ help="Configuration file to update",
237
+ ),
232
238
  env_file: Path = typer.Option(Path(".env"), "--env-file", help="Path to environment file"),
233
239
  project: Optional[str] = typer.Option(None, "--project", help="Project name under the FluxLoop root"),
234
240
  root: Path = typer.Option(Path(DEFAULT_ROOT_DIR_NAME), "--root", help="FluxLoop root directory"),
@@ -330,7 +336,11 @@ def validate(
330
336
  raise typer.Exit(1)
331
337
 
332
338
  try:
333
- config = load_experiment_config(config_file)
339
+ config = load_experiment_config(
340
+ resolved_path,
341
+ project=project,
342
+ root=root,
343
+ )
334
344
 
335
345
  # Show validation results
336
346
  console.print("[green]✓[/green] Configuration is valid!\n")
@@ -1,10 +1,9 @@
1
1
  """Generate command for producing input datasets."""
2
2
 
3
- import json
4
3
  import os
5
4
  import sys
6
5
  from pathlib import Path
7
- from typing import Any, Dict, List, Optional
6
+ from typing import Dict, List, Optional
8
7
 
9
8
  import typer
10
9
  from rich.console import Console
@@ -93,11 +92,6 @@ def inputs(
93
92
  "--llm-api-key",
94
93
  help="API key for LLM provider (falls back to FLUXLOOP_LLM_API_KEY)",
95
94
  ),
96
- from_recording: Optional[Path] = typer.Option(
97
- None,
98
- "--from-recording",
99
- help="Use a recorded call (JSONL) as template for generated inputs",
100
- ),
101
95
  ):
102
96
  """Generate input variations for review before running experiments."""
103
97
  resolved_config = resolve_config_path(config_file, project, root)
@@ -136,6 +130,8 @@ def inputs(
136
130
  try:
137
131
  config = load_experiment_config(
138
132
  resolved_config,
133
+ project=project,
134
+ root=root,
139
135
  require_inputs_file=require_inputs_file,
140
136
  )
141
137
  except Exception as exc:
@@ -201,20 +197,8 @@ def inputs(
201
197
  llm_api_key_override=llm_api_key,
202
198
  )
203
199
 
204
- recording_template = None
205
- if from_recording:
206
- try:
207
- recording_template = _load_recording_template(from_recording, config)
208
- console.print(
209
- "📝 Generating inputs from recording"
210
- f"\n → Base content: [cyan]{recording_template['base_content'][:60]}[/cyan]"
211
- )
212
- except Exception as exc:
213
- console.print(f"[red]Failed to load recording template:[/red] {exc}")
214
- raise typer.Exit(1)
215
-
216
200
  try:
217
- result = generate_inputs(config, settings, recording_template=recording_template)
201
+ result = generate_inputs(config, settings)
218
202
  except Exception as exc:
219
203
  console.print(f"[red]Generation failed:[/red] {exc}")
220
204
  if "--debug" in sys.argv:
@@ -238,67 +222,3 @@ def inputs(
238
222
  f"\n🧠 Mode: [magenta]{result.metadata.get('generation_mode', 'deterministic')}[/magenta]"
239
223
  f"\n🎯 Strategies: [cyan]{', '.join(strategy for strategy in strategies_used)}[/cyan]"
240
224
  )
241
-
242
-
243
- def _load_recording_template(path: Path, config: "ExperimentConfig") -> Dict[str, object]:
244
- target_path = path
245
- if not target_path.is_absolute():
246
- source_dir = config.get_source_dir()
247
- if source_dir:
248
- target_path = (source_dir / target_path).resolve()
249
- else:
250
- target_path = target_path.resolve()
251
-
252
- if not target_path.exists():
253
- raise FileNotFoundError(f"Recording file not found: {target_path}")
254
-
255
- with target_path.open("r", encoding="utf-8") as handle:
256
- first_line = handle.readline()
257
-
258
- if not first_line:
259
- raise ValueError(f"Recording file is empty: {target_path}")
260
-
261
- try:
262
- record = json.loads(first_line)
263
- except json.JSONDecodeError as exc:
264
- raise ValueError(f"Invalid JSON in recording file {target_path}: {exc}")
265
-
266
- kwargs = record.get("kwargs", {})
267
- base_content = _extract_content(kwargs)
268
- if not base_content:
269
- sample = json.dumps(kwargs, indent=2)[:500]
270
- raise ValueError(
271
- "Unable to locate textual content in recording. "
272
- "Expected keys such as data.content, input, input_text, message, query, text, or content." \
273
- f"\nRecording snapshot:\n{sample}"
274
- )
275
-
276
- return {
277
- "base_content": base_content,
278
- "full_kwargs": kwargs,
279
- "target": record.get("target"),
280
- }
281
-
282
-
283
- def _extract_content(kwargs: Dict[str, Any]) -> Optional[str]:
284
- content_paths = [
285
- "data.content",
286
- "input",
287
- "input_text",
288
- "message",
289
- "query",
290
- "text",
291
- "content",
292
- ]
293
-
294
- for path in content_paths:
295
- try:
296
- current: Any = kwargs
297
- for part in path.split('.'):
298
- current = current[part]
299
- if isinstance(current, str) and current:
300
- return current
301
- except (KeyError, TypeError):
302
- continue
303
-
304
- return None
@@ -11,13 +11,21 @@ from rich.prompt import Confirm
11
11
  from rich.tree import Tree
12
12
 
13
13
  from ..templates import (
14
- create_experiment_config,
14
+ create_project_config,
15
+ create_input_config,
16
+ create_simulation_config,
17
+ create_evaluation_config,
15
18
  create_sample_agent,
16
19
  create_gitignore,
17
20
  create_env_file,
18
21
  )
19
22
  from ..project_paths import resolve_root_dir, resolve_project_dir
20
- from ..constants import DEFAULT_ROOT_DIR_NAME
23
+ from ..constants import (
24
+ DEFAULT_ROOT_DIR_NAME,
25
+ CONFIG_DIRECTORY_NAME,
26
+ CONFIG_SECTION_FILENAMES,
27
+ CONFIG_SECTION_ORDER,
28
+ )
21
29
 
22
30
  app = typer.Typer()
23
31
  console = Console()
@@ -51,7 +59,7 @@ def project(
51
59
  Initialize a new FluxLoop project.
52
60
 
53
61
  This command creates:
54
- - setting.yaml: Default experiment configuration
62
+ - configs/: Separated configuration files (project/input/simulation/evaluation)
55
63
  - .env: Environment variables template
56
64
  - examples/: Sample agent code (optional)
57
65
  """
@@ -89,14 +97,20 @@ def project(
89
97
  raise typer.Abort()
90
98
 
91
99
  # Check for existing files
92
- config_file = project_path / "setting.yaml"
100
+ config_dir = project_path / CONFIG_DIRECTORY_NAME
101
+ section_paths = {
102
+ key: config_dir / CONFIG_SECTION_FILENAMES[key]
103
+ for key in CONFIG_SECTION_FILENAMES
104
+ }
93
105
  env_file = project_path / ".env"
94
106
  gitignore_file = project_path / ".gitignore"
95
107
 
96
108
  if not force:
97
109
  existing_files = []
98
- if config_file.exists():
99
- existing_files.append("setting.yaml")
110
+ for key in CONFIG_SECTION_ORDER:
111
+ path = section_paths[key]
112
+ if path.exists():
113
+ existing_files.append(path.relative_to(project_path).as_posix())
100
114
  if env_file.exists():
101
115
  existing_files.append(".env")
102
116
 
@@ -107,10 +121,21 @@ def project(
107
121
  if not Confirm.ask("Overwrite existing files?", default=False):
108
122
  raise typer.Abort()
109
123
 
110
- # Create configuration file
111
- console.print("📝 Creating experiment configuration...")
112
- config_content = create_experiment_config(project_name)
113
- config_file.write_text(config_content)
124
+ # Create configuration files
125
+ console.print("📝 Creating configuration files...")
126
+ config_dir.mkdir(exist_ok=True)
127
+
128
+ section_writers = {
129
+ "project": lambda: create_project_config(project_name),
130
+ "input": create_input_config,
131
+ "simulation": lambda: create_simulation_config(project_name),
132
+ "evaluation": create_evaluation_config,
133
+ }
134
+
135
+ for key in CONFIG_SECTION_ORDER:
136
+ content = section_writers[key]() # type: ignore[operator]
137
+ section_path = section_paths[key]
138
+ section_path.write_text(content)
114
139
 
115
140
  # Ensure root .env exists, create project override template
116
141
  root_env_file = root_dir / ".env"
@@ -145,7 +170,9 @@ def project(
145
170
  console.print("\n[bold green]✓ Project initialized successfully![/bold green]\n")
146
171
 
147
172
  tree = Tree(f"[bold]{project_name}/[/bold]")
148
- tree.add("📄 setting.yaml")
173
+ configs_node = tree.add(f"📁 {CONFIG_DIRECTORY_NAME}/")
174
+ for key in CONFIG_SECTION_ORDER:
175
+ configs_node.add(f"📄 {CONFIG_SECTION_FILENAMES[key]}")
149
176
  tree.add("🔐 .env")
150
177
  tree.add("📄 .gitignore")
151
178
  tree.add("📁 recordings/")
@@ -158,7 +185,7 @@ def project(
158
185
 
159
186
  # Show next steps
160
187
  console.print("\n[bold]Next steps:[/bold]")
161
- console.print("1. Edit [cyan]setting.yaml[/cyan] to configure your experiment")
188
+ console.print("1. Review configs in [cyan]configs/[/cyan] (project/input/simulation/evaluation)")
162
189
  console.print("2. Set up environment variables in [cyan].env[/cyan]")
163
190
  if with_example:
164
191
  console.print("3. Try running: [green]fluxloop run experiment[/green]")
@@ -0,0 +1,150 @@
1
+ """Record command for managing argument recording mode."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Dict, Optional
7
+
8
+ import typer
9
+ from rich.console import Console
10
+ from rich.table import Table
11
+
12
+ from ..constants import DEFAULT_ROOT_DIR_NAME
13
+ from ..project_paths import (
14
+ resolve_env_path,
15
+ resolve_project_dir,
16
+ resolve_config_section_path,
17
+ )
18
+ from ..config_schema import CONFIG_SECTION_FILENAMES
19
+
20
+
21
+ app = typer.Typer()
22
+ console = Console()
23
+
24
+
25
+ def _load_env(env_path: Path) -> Dict[str, str]:
26
+ values: Dict[str, str] = {}
27
+ if env_path.exists():
28
+ for line in env_path.read_text().splitlines():
29
+ line = line.strip()
30
+ if not line or line.startswith("#") or "=" not in line:
31
+ continue
32
+ key, value = line.split("=", 1)
33
+ values[key.strip()] = value.strip()
34
+ return values
35
+
36
+
37
+ def _write_env(env_path: Path, values: Dict[str, str]) -> None:
38
+ env_path.write_text("\n".join(f"{k}={v}" for k, v in values.items()) + "\n")
39
+
40
+
41
+ def _update_simulation(recording_enabled: bool, project: Optional[str], root: Path) -> None:
42
+ try:
43
+ simulation_path = resolve_config_section_path("simulation", project, root)
44
+ except KeyError:
45
+ return
46
+
47
+ data: Dict[str, object] = {}
48
+ if simulation_path.exists():
49
+ import yaml
50
+
51
+ data = yaml.safe_load(simulation_path.read_text()) or {}
52
+
53
+ replay = data.setdefault("replay_args", {})
54
+ replay["enabled"] = recording_enabled
55
+
56
+ import yaml
57
+
58
+ simulation_path.parent.mkdir(parents=True, exist_ok=True)
59
+ simulation_path.write_text(
60
+ yaml.dump(data, sort_keys=False, default_flow_style=False),
61
+ encoding="utf-8",
62
+ )
63
+
64
+
65
+ @app.command()
66
+ def enable(
67
+ project: Optional[str] = typer.Option(None, "--project", help="Project name"),
68
+ root: Path = typer.Option(Path(DEFAULT_ROOT_DIR_NAME), "--root", help="FluxLoop root directory"),
69
+ recording_file: Path = typer.Option(
70
+ Path("recordings/args_recording.jsonl"),
71
+ "--file",
72
+ help="Recording file path relative to project",
73
+ ),
74
+ ):
75
+ """Enable recording mode by updating .env and simulation config."""
76
+
77
+ env_path = resolve_env_path(Path(".env"), project, root)
78
+ env_values = _load_env(env_path)
79
+ env_values["FLUXLOOP_RECORD_ARGS"] = "true"
80
+ env_values["FLUXLOOP_RECORDING_FILE"] = str(recording_file)
81
+
82
+ env_path.parent.mkdir(parents=True, exist_ok=True)
83
+ _write_env(env_path, env_values)
84
+
85
+ _update_simulation(True, project, root)
86
+
87
+ console.print(
88
+ f"[green]✓[/green] Recording enabled. Edit {env_path} or configs/simulation.yaml to adjust settings."
89
+ )
90
+
91
+
92
+ @app.command()
93
+ def disable(
94
+ project: Optional[str] = typer.Option(None, "--project", help="Project name"),
95
+ root: Path = typer.Option(Path(DEFAULT_ROOT_DIR_NAME), "--root", help="FluxLoop root directory"),
96
+ ):
97
+ """Disable recording mode and reset settings."""
98
+
99
+ env_path = resolve_env_path(Path(".env"), project, root)
100
+ env_values = _load_env(env_path)
101
+ env_values["FLUXLOOP_RECORD_ARGS"] = "false"
102
+ env_path.parent.mkdir(parents=True, exist_ok=True)
103
+ _write_env(env_path, env_values)
104
+
105
+ _update_simulation(False, project, root)
106
+
107
+ console.print("[green]✓[/green] Recording disabled.")
108
+
109
+
110
+ @app.command()
111
+ def status(
112
+ project: Optional[str] = typer.Option(None, "--project", help="Project name"),
113
+ root: Path = typer.Option(Path(DEFAULT_ROOT_DIR_NAME), "--root", help="FluxLoop root directory"),
114
+ ):
115
+ """Display current recording status."""
116
+
117
+ env_path = resolve_env_path(Path(".env"), project, root)
118
+ env_values = _load_env(env_path)
119
+ record_args = env_values.get("FLUXLOOP_RECORD_ARGS", "false").lower() == "true"
120
+ recording_file = env_values.get("FLUXLOOP_RECORDING_FILE", "(default)")
121
+
122
+ simulation_path = resolve_config_section_path("simulation", project, root)
123
+ simulation_exists = simulation_path.exists()
124
+
125
+ table = Table(title="Recording Status", show_header=False)
126
+ table.add_column("Property", style="cyan")
127
+ table.add_column("Value")
128
+
129
+ table.add_row("Mode", "ENABLED" if record_args else "disabled")
130
+ table.add_row("Recording File", recording_file)
131
+ table.add_row(".env Path", str(env_path))
132
+ table.add_row("Simulation Config", str(simulation_path) if simulation_exists else "(missing)")
133
+
134
+ console.print(table)
135
+
136
+ project_dir = (
137
+ resolve_project_dir(project, root)
138
+ if project
139
+ else Path.cwd()
140
+ )
141
+ recordings_dir = project_dir / Path(recording_file).parent
142
+
143
+ if recordings_dir.exists():
144
+ console.print(
145
+ f"\n[dim]Recording directory:[/dim] {recordings_dir}\n"
146
+ )
147
+ else:
148
+ console.print(
149
+ f"\n[yellow]Recording directory does not exist yet:[/yellow] {recordings_dir}\n"
150
+ )
@@ -94,7 +94,11 @@ def experiment(
94
94
  console.print(f"📋 Loading configuration from: [cyan]{resolved_config}[/cyan]")
95
95
 
96
96
  try:
97
- config = load_experiment_config(resolved_config)
97
+ config = load_experiment_config(
98
+ resolved_config,
99
+ project=project,
100
+ root=root,
101
+ )
98
102
  except Exception as e:
99
103
  console.print(f"[red]Error loading configuration:[/red] {e}")
100
104
  raise typer.Exit(1)
@@ -11,7 +11,13 @@ from rich.table import Table
11
11
  from rich.panel import Panel
12
12
 
13
13
  from ..constants import DEFAULT_CONFIG_PATH, DEFAULT_ROOT_DIR_NAME
14
- from ..project_paths import resolve_config_path, resolve_root_dir, resolve_project_relative
14
+ from ..config_schema import CONFIG_SECTION_FILENAMES, CONFIG_REQUIRED_KEYS
15
+ from ..project_paths import (
16
+ resolve_config_path,
17
+ resolve_config_directory,
18
+ resolve_root_dir,
19
+ resolve_project_relative,
20
+ )
15
21
 
16
22
  app = typer.Typer()
17
23
  console = Console()
@@ -90,17 +96,40 @@ def check(
90
96
 
91
97
  # Check for configuration file
92
98
  resolved_config = resolve_config_path(DEFAULT_CONFIG_PATH, project, root)
93
- if resolved_config.exists():
99
+ config_dir = resolve_config_directory(project, root)
100
+
101
+ missing_sections = [
102
+ CONFIG_SECTION_FILENAMES[key]
103
+ for key in CONFIG_SECTION_FILENAMES
104
+ if not (config_dir / CONFIG_SECTION_FILENAMES[key]).exists()
105
+ ]
106
+
107
+ if resolved_config.exists() and not missing_sections:
94
108
  status_table.add_row(
95
109
  "Config",
96
110
  "[green]✓ Found[/green]",
97
- str(resolved_config)
111
+ str(config_dir)
98
112
  )
99
113
  else:
114
+ detail_parts = []
115
+ if not config_dir.exists():
116
+ detail_parts.append(f"Missing directory: {config_dir}")
117
+ if missing_sections:
118
+ required_missing = [
119
+ name for name in missing_sections
120
+ if _section_key_from_filename(name) in CONFIG_REQUIRED_KEYS
121
+ ]
122
+ if required_missing:
123
+ detail_parts.append(
124
+ "Missing sections: " + ", ".join(required_missing)
125
+ )
126
+ if not detail_parts:
127
+ detail_parts.append("Run: fluxloop init project")
128
+
100
129
  status_table.add_row(
101
130
  "Config",
102
- "[yellow]- Not found[/yellow]",
103
- "Run: fluxloop init project"
131
+ "[yellow]- Incomplete[/yellow]",
132
+ "\n".join(detail_parts)
104
133
  )
105
134
 
106
135
  # Check environment
@@ -133,6 +162,13 @@ def check(
133
162
  console.print(f" • {rec}")
134
163
 
135
164
 
165
+ def _section_key_from_filename(filename: str) -> Optional[str]:
166
+ for key, value in CONFIG_SECTION_FILENAMES.items():
167
+ if value == filename:
168
+ return key
169
+ return None
170
+
171
+
136
172
  @app.command()
137
173
  def experiments(
138
174
  output_dir: Path = typer.Option(