monoco-toolkit 0.1.1__py3-none-any.whl → 0.2.8__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.
Files changed (76) hide show
  1. monoco/cli/__init__.py +0 -0
  2. monoco/cli/project.py +87 -0
  3. monoco/cli/workspace.py +46 -0
  4. monoco/core/agent/__init__.py +5 -0
  5. monoco/core/agent/action.py +144 -0
  6. monoco/core/agent/adapters.py +129 -0
  7. monoco/core/agent/protocol.py +31 -0
  8. monoco/core/agent/state.py +106 -0
  9. monoco/core/config.py +212 -17
  10. monoco/core/execution.py +62 -0
  11. monoco/core/feature.py +58 -0
  12. monoco/core/git.py +51 -2
  13. monoco/core/injection.py +196 -0
  14. monoco/core/integrations.py +242 -0
  15. monoco/core/lsp.py +68 -0
  16. monoco/core/output.py +21 -3
  17. monoco/core/registry.py +36 -0
  18. monoco/core/resources/en/AGENTS.md +8 -0
  19. monoco/core/resources/en/SKILL.md +66 -0
  20. monoco/core/resources/zh/AGENTS.md +8 -0
  21. monoco/core/resources/zh/SKILL.md +65 -0
  22. monoco/core/setup.py +96 -110
  23. monoco/core/skills.py +444 -0
  24. monoco/core/state.py +53 -0
  25. monoco/core/sync.py +224 -0
  26. monoco/core/telemetry.py +4 -1
  27. monoco/core/workspace.py +85 -20
  28. monoco/daemon/app.py +127 -58
  29. monoco/daemon/models.py +4 -0
  30. monoco/daemon/services.py +56 -155
  31. monoco/features/config/commands.py +125 -44
  32. monoco/features/i18n/adapter.py +29 -0
  33. monoco/features/i18n/commands.py +89 -10
  34. monoco/features/i18n/core.py +113 -27
  35. monoco/features/i18n/resources/en/AGENTS.md +8 -0
  36. monoco/features/i18n/resources/en/SKILL.md +94 -0
  37. monoco/features/i18n/resources/zh/AGENTS.md +8 -0
  38. monoco/features/i18n/resources/zh/SKILL.md +94 -0
  39. monoco/features/issue/adapter.py +34 -0
  40. monoco/features/issue/commands.py +343 -101
  41. monoco/features/issue/core.py +384 -150
  42. monoco/features/issue/domain/__init__.py +0 -0
  43. monoco/features/issue/domain/lifecycle.py +126 -0
  44. monoco/features/issue/domain/models.py +170 -0
  45. monoco/features/issue/domain/parser.py +223 -0
  46. monoco/features/issue/domain/workspace.py +104 -0
  47. monoco/features/issue/engine/__init__.py +22 -0
  48. monoco/features/issue/engine/config.py +172 -0
  49. monoco/features/issue/engine/machine.py +185 -0
  50. monoco/features/issue/engine/models.py +18 -0
  51. monoco/features/issue/linter.py +325 -120
  52. monoco/features/issue/lsp/__init__.py +3 -0
  53. monoco/features/issue/lsp/definition.py +72 -0
  54. monoco/features/issue/migration.py +134 -0
  55. monoco/features/issue/models.py +46 -24
  56. monoco/features/issue/monitor.py +94 -0
  57. monoco/features/issue/resources/en/AGENTS.md +20 -0
  58. monoco/features/issue/resources/en/SKILL.md +111 -0
  59. monoco/features/issue/resources/zh/AGENTS.md +20 -0
  60. monoco/features/issue/resources/zh/SKILL.md +138 -0
  61. monoco/features/issue/validator.py +455 -0
  62. monoco/features/spike/adapter.py +30 -0
  63. monoco/features/spike/commands.py +45 -24
  64. monoco/features/spike/core.py +6 -40
  65. monoco/features/spike/resources/en/AGENTS.md +7 -0
  66. monoco/features/spike/resources/en/SKILL.md +74 -0
  67. monoco/features/spike/resources/zh/AGENTS.md +7 -0
  68. monoco/features/spike/resources/zh/SKILL.md +74 -0
  69. monoco/main.py +91 -2
  70. monoco_toolkit-0.2.8.dist-info/METADATA +136 -0
  71. monoco_toolkit-0.2.8.dist-info/RECORD +83 -0
  72. monoco_toolkit-0.1.1.dist-info/METADATA +0 -93
  73. monoco_toolkit-0.1.1.dist-info/RECORD +0 -33
  74. {monoco_toolkit-0.1.1.dist-info → monoco_toolkit-0.2.8.dist-info}/WHEEL +0 -0
  75. {monoco_toolkit-0.1.1.dist-info → monoco_toolkit-0.2.8.dist-info}/entry_points.txt +0 -0
  76. {monoco_toolkit-0.1.1.dist-info → monoco_toolkit-0.2.8.dist-info}/licenses/LICENSE +0 -0
monoco/cli/__init__.py ADDED
File without changes
monoco/cli/project.py ADDED
@@ -0,0 +1,87 @@
1
+ import typer
2
+ from pathlib import Path
3
+ from typing import Optional, Annotated
4
+ from rich.console import Console
5
+ from rich.table import Table
6
+ import yaml
7
+ import json
8
+ import os
9
+
10
+ from monoco.core.workspace import find_projects, is_project_root
11
+ from monoco.core.config import get_config
12
+ from monoco.core.output import AgentOutput, OutputManager
13
+
14
+ app = typer.Typer(help="Manage Monoco Projects")
15
+ console = Console()
16
+
17
+ @app.command("list")
18
+ def list_projects(
19
+ json: AgentOutput = False,
20
+ root: Optional[str] = typer.Option(None, "--root", help="Workspace root")
21
+ ):
22
+ """List all discovered projects in the workspace."""
23
+ cwd = Path(root).resolve() if root else Path.cwd()
24
+ projects = find_projects(cwd)
25
+
26
+ if OutputManager.is_agent_mode():
27
+ data = [
28
+ {
29
+ "id": p.id,
30
+ "name": p.name,
31
+ "path": str(p.path),
32
+ "key": p.config.project.key if p.config.project else ""
33
+ }
34
+ for p in projects
35
+ ]
36
+ OutputManager.print(data)
37
+ else:
38
+ table = Table(title=f"Projects in {cwd}")
39
+ table.add_column("ID", style="cyan")
40
+ table.add_column("Name", style="magenta")
41
+ table.add_column("Key", style="green")
42
+ table.add_column("Path", style="dim")
43
+
44
+ for p in projects:
45
+ path_str = str(p.path.relative_to(cwd)) if p.path.is_relative_to(cwd) else str(p.path)
46
+ if path_str == ".":
47
+ path_str = "(root)"
48
+ key = p.config.project.key if p.config.project else "N/A"
49
+ table.add_row(p.id, p.name, key, path_str)
50
+
51
+ console.print(table)
52
+
53
+ @app.command("init")
54
+ def init_project(
55
+ name: str = typer.Option(..., "--name", "-n", help="Project Name"),
56
+ key: str = typer.Option(..., "--key", "-k", help="Project Key"),
57
+ force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing config"),
58
+ json: AgentOutput = False,
59
+ ):
60
+ """Initialize a new project in the current directory."""
61
+ cwd = Path.cwd()
62
+ project_config_path = cwd / ".monoco" / "project.yaml"
63
+
64
+ if project_config_path.exists() and not force:
65
+ OutputManager.error(f"Project already initialized in {cwd}. Use --force to overwrite.")
66
+ raise typer.Exit(code=1)
67
+
68
+ cwd.mkdir(parents=True, exist_ok=True)
69
+ (cwd / ".monoco").mkdir(exist_ok=True)
70
+
71
+ config = {
72
+ "project": {
73
+ "name": name,
74
+ "key": key
75
+ }
76
+ }
77
+
78
+ with open(project_config_path, "w") as f:
79
+ yaml.dump(config, f, default_flow_style=False)
80
+
81
+ OutputManager.print({
82
+ "status": "initialized",
83
+ "name": name,
84
+ "key": key,
85
+ "path": str(cwd),
86
+ "config_file": str(project_config_path)
87
+ })
@@ -0,0 +1,46 @@
1
+ import typer
2
+ from pathlib import Path
3
+ from rich.console import Console
4
+ from typing import Annotated
5
+ import yaml
6
+ import json
7
+
8
+ from monoco.core.output import AgentOutput, OutputManager
9
+
10
+ app = typer.Typer(help="Manage Monoco Workspace")
11
+ console = Console()
12
+
13
+ @app.command("init")
14
+ def init_workspace(
15
+ force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing config"),
16
+ json: AgentOutput = False,
17
+ ):
18
+ """Initialize a workspace environment in the current directory."""
19
+ cwd = Path.cwd()
20
+ workspace_config_path = cwd / ".monoco" / "workspace.yaml"
21
+
22
+ if workspace_config_path.exists() and not force:
23
+ OutputManager.error(f"Workspace already initialized in {cwd}. Use --force to overwrite.")
24
+ raise typer.Exit(code=1)
25
+
26
+ cwd.mkdir(parents=True, exist_ok=True)
27
+ (cwd / ".monoco").mkdir(exist_ok=True)
28
+
29
+ # Default workspace config
30
+ config = {
31
+ "paths": {
32
+ "issues": "Issues", # Default
33
+ "spikes": ".references",
34
+ "specs": "SPECS"
35
+ }
36
+ }
37
+
38
+ with open(workspace_config_path, "w") as f:
39
+ yaml.dump(config, f, default_flow_style=False)
40
+
41
+ OutputManager.print({
42
+ "status": "initialized",
43
+ "path": str(cwd),
44
+ "config_file": str(workspace_config_path)
45
+ })
46
+
@@ -0,0 +1,5 @@
1
+ """
2
+ Monoco Agent Execution Layer.
3
+ """
4
+
5
+ from .state import AgentStateManager, AgentState, AgentProviderState
@@ -0,0 +1,144 @@
1
+
2
+ import re
3
+ import yaml
4
+ from pathlib import Path
5
+ from typing import Dict, List, Optional, Any
6
+ from pydantic import BaseModel, Field
7
+
8
+ class ActionContext(BaseModel):
9
+ """Context information for matching actions."""
10
+ id: Optional[str] = None
11
+ type: Optional[str] = None
12
+ stage: Optional[str] = None
13
+ status: Optional[str] = None
14
+ file_path: Optional[str] = None
15
+ project_id: Optional[str] = None
16
+
17
+ class ActionWhen(BaseModel):
18
+ """Conditions under which an action should be displayed/active."""
19
+ idMatch: Optional[str] = None
20
+ typeMatch: Optional[str] = None
21
+ stageMatch: Optional[str] = None
22
+ statusMatch: Optional[str] = None
23
+ fileMatch: Optional[str] = None
24
+
25
+ def matches(self, context: ActionContext) -> bool:
26
+ """Evaluate if the context matches these criteria."""
27
+ if self.idMatch and context.id and not re.match(self.idMatch, context.id):
28
+ return False
29
+ if self.typeMatch and context.type and not re.match(self.typeMatch, context.type):
30
+ return False
31
+ if self.stageMatch and context.stage and not re.match(self.stageMatch, context.stage):
32
+ return False
33
+ if self.statusMatch and context.status and not re.match(self.statusMatch, context.status):
34
+ return False
35
+ if self.fileMatch and context.file_path and not re.match(self.fileMatch, context.file_path):
36
+ return False
37
+ return True
38
+
39
+ class PromptyAction(BaseModel):
40
+ name: str
41
+ description: str
42
+ version: Optional[str] = "1.0.0"
43
+ authors: List[str] = []
44
+ model: Dict[str, Any] = {}
45
+ inputs: Dict[str, Any] = {}
46
+ outputs: Dict[str, Any] = {}
47
+ template: str
48
+ when: Optional[ActionWhen] = None
49
+
50
+ # Monoco specific metadata
51
+ path: Optional[str] = None
52
+ provider: Optional[str] = None # Derived from model.api or explicitly set
53
+
54
+ class ActionRegistry:
55
+ def __init__(self, project_root: Optional[Path] = None):
56
+ self.project_root = project_root
57
+ self._actions: List[PromptyAction] = []
58
+
59
+ def scan(self) -> List[PromptyAction]:
60
+ """Scan user global and project local directories for .prompty files."""
61
+ self._actions = []
62
+
63
+ # 1. User Global: ~/.monoco/actions/
64
+ user_dir = Path.home() / ".monoco" / "actions"
65
+ self._scan_dir(user_dir)
66
+
67
+ # 2. Project Local: {project_root}/.monoco/actions/
68
+ if self.project_root:
69
+ project_dir = self.project_root / ".monoco" / "actions"
70
+ self._scan_dir(project_dir)
71
+
72
+ return self._actions
73
+
74
+ def _scan_dir(self, directory: Path):
75
+ if not directory.exists():
76
+ return
77
+
78
+ for prompty_file in directory.glob("*.prompty"):
79
+ try:
80
+ action = self._load_action(prompty_file)
81
+ if action:
82
+ self._actions.append(action)
83
+ except Exception as e:
84
+ print(f"Failed to load action {prompty_file}: {e}")
85
+
86
+ def _load_action(self, file_path: Path) -> Optional[PromptyAction]:
87
+ content = file_path.read_text(encoding="utf-8")
88
+
89
+ # Prompty Parser (Standard YAML Frontmatter + Body)
90
+ # We look for the first --- and the second ---
91
+ parts = re.split(r"^---\s*$", content, maxsplit=2, flags=re.MULTILINE)
92
+
93
+ if len(parts) < 3:
94
+ return None
95
+
96
+ frontmatter_raw = parts[1]
97
+ body = parts[2].strip()
98
+
99
+ try:
100
+ meta = yaml.safe_load(frontmatter_raw)
101
+ if not meta or "name" not in meta:
102
+ # Use filename as fallback name if missing? Prompty usually requires name.
103
+ if not meta: meta = {}
104
+ meta["name"] = meta.get("name", file_path.stem)
105
+
106
+ # Map Prompty 'when' if present
107
+ when_data = meta.get("when")
108
+ when = ActionWhen(**when_data) if when_data else None
109
+
110
+ action = PromptyAction(
111
+ name=meta["name"],
112
+ description=meta.get("description", ""),
113
+ version=meta.get("version"),
114
+ authors=meta.get("authors", []),
115
+ model=meta.get("model", {}),
116
+ inputs=meta.get("inputs", {}),
117
+ outputs=meta.get("outputs", {}),
118
+ template=body,
119
+ when=when,
120
+ path=str(file_path.absolute()),
121
+ provider=meta.get("provider") or meta.get("model", {}).get("api")
122
+ )
123
+ return action
124
+
125
+ except Exception as e:
126
+ print(f"Invalid Prompty in {file_path}: {e}")
127
+ return None
128
+
129
+ def list_available(self, context: Optional[ActionContext] = None) -> List[PromptyAction]:
130
+ if not self._actions:
131
+ self.scan()
132
+
133
+ if not context:
134
+ return self._actions
135
+
136
+ return [a for a in self._actions if not a.when or a.when.matches(context)]
137
+
138
+ def get(self, name: str) -> Optional[PromptyAction]:
139
+ if not self._actions:
140
+ self.scan()
141
+ for a in self._actions:
142
+ if a.name == name:
143
+ return a
144
+ return None
@@ -0,0 +1,129 @@
1
+ """
2
+ CLI Adapters for Agent Frameworks.
3
+ """
4
+
5
+ import shutil
6
+ import subprocess
7
+ from typing import List
8
+ from pathlib import Path
9
+ from .protocol import AgentClient
10
+
11
+ class BaseCLIClient:
12
+ def __init__(self, executable: str):
13
+ self._executable = executable
14
+
15
+ @property
16
+ def name(self) -> str:
17
+ return self._executable
18
+
19
+ async def available(self) -> bool:
20
+ return shutil.which(self._executable) is not None
21
+
22
+ def _build_prompt(self, prompt: str, context_files: List[Path]) -> str:
23
+ """Concatenate prompt and context files."""
24
+ # Inject Language Rule
25
+ try:
26
+ from monoco.core.config import get_config
27
+ settings = get_config()
28
+ lang = settings.i18n.source_lang
29
+ if lang:
30
+ prompt = f"{prompt}\n\n[SYSTEM: LANGUAGE CONSTRAINT]\nThe project source language is '{lang}'. You MUST use '{lang}' for all thinking and reporting unless explicitly instructed otherwise."
31
+ except Exception:
32
+ pass
33
+
34
+ full_prompt = [prompt]
35
+ if context_files:
36
+ full_prompt.append("\n\n--- CONTEXT FILES ---")
37
+ for file_path in context_files:
38
+ try:
39
+ full_prompt.append(f"\nFile: {file_path}")
40
+ full_prompt.append("```")
41
+ # Read file content safely
42
+ full_prompt.append(file_path.read_text(encoding="utf-8", errors="replace"))
43
+ full_prompt.append("```")
44
+ except Exception as e:
45
+ full_prompt.append(f"Error reading {file_path}: {e}")
46
+ full_prompt.append("--- END CONTEXT ---\n")
47
+ return "\n".join(full_prompt)
48
+
49
+ async def _run_command(self, args: List[str]) -> str:
50
+ """Run the CLI command and return stdout."""
51
+ # Using synchronous subprocess in async function for now
52
+ # Ideally this should use asyncio.create_subprocess_exec
53
+ import asyncio
54
+
55
+ proc = await asyncio.create_subprocess_exec(
56
+ *args,
57
+ stdout=asyncio.subprocess.PIPE,
58
+ stderr=asyncio.subprocess.PIPE
59
+ )
60
+
61
+ stdout, stderr = await proc.communicate()
62
+
63
+ if proc.returncode != 0:
64
+ error_msg = stderr.decode().strip()
65
+ raise RuntimeError(f"Agent CLI failed (code {proc.returncode}): {error_msg}")
66
+
67
+ return stdout.decode().strip()
68
+
69
+
70
+ class GeminiClient(BaseCLIClient, AgentClient):
71
+ """Adapter for Google Gemini CLI."""
72
+
73
+ def __init__(self):
74
+ super().__init__("gemini")
75
+
76
+ async def execute(self, prompt: str, context_files: List[Path] = []) -> str:
77
+ full_prompt = self._build_prompt(prompt, context_files)
78
+ # Usage: gemini "prompt"
79
+ return await self._run_command([self._executable, full_prompt])
80
+
81
+
82
+ class ClaudeClient(BaseCLIClient, AgentClient):
83
+ """Adapter for Anthropic Claude CLI."""
84
+
85
+ def __init__(self):
86
+ super().__init__("claude")
87
+
88
+ async def execute(self, prompt: str, context_files: List[Path] = []) -> str:
89
+ full_prompt = self._build_prompt(prompt, context_files)
90
+ # Usage: claude -p "prompt"
91
+ return await self._run_command([self._executable, "-p", full_prompt])
92
+
93
+
94
+ class QwenClient(BaseCLIClient, AgentClient):
95
+ """Adapter for Alibaba Qwen CLI."""
96
+
97
+ def __init__(self):
98
+ super().__init__("qwen")
99
+
100
+ async def execute(self, prompt: str, context_files: List[Path] = []) -> str:
101
+ full_prompt = self._build_prompt(prompt, context_files)
102
+ # Usage: qwen "prompt"
103
+ return await self._run_command([self._executable, full_prompt])
104
+
105
+
106
+ class KimiClient(BaseCLIClient, AgentClient):
107
+ """Adapter for Moonshot Kimi CLI."""
108
+
109
+ def __init__(self):
110
+ super().__init__("kimi")
111
+
112
+ async def execute(self, prompt: str, context_files: List[Path] = []) -> str:
113
+ full_prompt = self._build_prompt(prompt, context_files)
114
+ # Usage: kimi "prompt"
115
+ return await self._run_command([self._executable, full_prompt])
116
+
117
+
118
+ _ADAPTERS = {
119
+ "gemini": GeminiClient,
120
+ "claude": ClaudeClient,
121
+ "qwen": QwenClient,
122
+ "kimi": KimiClient
123
+ }
124
+
125
+ def get_agent_client(name: str) -> AgentClient:
126
+ """Factory to get agent client by name."""
127
+ if name not in _ADAPTERS:
128
+ raise ValueError(f"Unknown agent provider: {name}")
129
+ return _ADAPTERS[name]()
@@ -0,0 +1,31 @@
1
+ """
2
+ Protocol definition for Agent Clients.
3
+ """
4
+
5
+ from typing import Protocol, List, Optional
6
+ from pathlib import Path
7
+
8
+ class AgentClient(Protocol):
9
+ """Protocol for interacting with CLI-based agents."""
10
+
11
+ @property
12
+ def name(self) -> str:
13
+ """Name of the agent provider (e.g. 'gemini', 'claude')."""
14
+ ...
15
+
16
+ async def available(self) -> bool:
17
+ """Check if the agent is available in the current environment."""
18
+ ...
19
+
20
+ async def execute(self, prompt: str, context_files: List[Path] = []) -> str:
21
+ """
22
+ Execute a prompt against the agent.
23
+
24
+ Args:
25
+ prompt: The main instructions.
26
+ context_files: List of files to provide as context.
27
+
28
+ Returns:
29
+ The raw string response from the agent.
30
+ """
31
+ ...
@@ -0,0 +1,106 @@
1
+ """
2
+ Agent State Management.
3
+
4
+ Handles persistence and retrieval of agent availability state.
5
+ """
6
+
7
+ import yaml
8
+ import shutil
9
+ import subprocess
10
+ import logging
11
+ from pathlib import Path
12
+ from datetime import datetime, timezone
13
+ from typing import Dict, Optional
14
+ from pydantic import BaseModel, Field
15
+
16
+ logger = logging.getLogger("monoco.core.agent.state")
17
+
18
+ class AgentProviderState(BaseModel):
19
+ available: bool
20
+ path: Optional[str] = None
21
+ error: Optional[str] = None
22
+ latency_ms: Optional[int] = None
23
+
24
+ class AgentState(BaseModel):
25
+ last_checked: datetime
26
+ providers: Dict[str, AgentProviderState]
27
+
28
+ @property
29
+ def is_stale(self) -> bool:
30
+ """Check if the state is older than 7 days."""
31
+ delta = datetime.now(timezone.utc) - self.last_checked
32
+ return delta.days > 7
33
+
34
+ class AgentStateManager:
35
+ def __init__(self, state_path: Path = Path.home() / ".monoco" / "agent_state.yaml"):
36
+ self.state_path = state_path
37
+ self._state: Optional[AgentState] = None
38
+
39
+ def load(self) -> Optional[AgentState]:
40
+ """Load state from file, returning None if missing or invalid."""
41
+ if not self.state_path.exists():
42
+ return None
43
+
44
+ try:
45
+ with open(self.state_path, "r") as f:
46
+ data = yaml.safe_load(f)
47
+ if not data:
48
+ return None
49
+ # Handle ISO string to datetime conversion if needed provided by Pydantic mostly
50
+ return AgentState(**data)
51
+ except Exception as e:
52
+ logger.warning(f"Failed to load agent state: {e}")
53
+ return None
54
+
55
+ def get_or_refresh(self, force: bool = False) -> AgentState:
56
+ """Get current state, refreshing if missing, stale, or forced."""
57
+ if not force:
58
+ self._state = self.load()
59
+ if self._state and not self._state.is_stale:
60
+ return self._state
61
+
62
+ return self.refresh()
63
+
64
+ def refresh(self) -> AgentState:
65
+ """Run diagnostics on all integrations and update state."""
66
+ logger.info("Refreshing agent state...")
67
+
68
+ from monoco.core.integrations import get_all_integrations
69
+ from monoco.core.config import get_config
70
+
71
+ # Load config to get possible overrides
72
+ # Determine root (hacky for now, should be passed)
73
+ root = Path.cwd()
74
+ config = get_config(str(root))
75
+
76
+ integrations = get_all_integrations(config_overrides=config.agent.integrations, enabled_only=True)
77
+
78
+ providers = {}
79
+ for key, integration in integrations.items():
80
+ if not integration.bin_name:
81
+ continue # Skip integrations that don't have a binary component
82
+
83
+ health = integration.check_health()
84
+ providers[key] = AgentProviderState(
85
+ available=health.available,
86
+ path=health.path,
87
+ error=health.error,
88
+ latency_ms=health.latency_ms
89
+ )
90
+
91
+ state = AgentState(
92
+ last_checked=datetime.now(timezone.utc),
93
+ providers=providers
94
+ )
95
+
96
+ # Save state
97
+ self.state_path.parent.mkdir(parents=True, exist_ok=True)
98
+ with open(self.state_path, "w") as f:
99
+ yaml.dump(state.model_dump(mode='json'), f)
100
+
101
+ self._state = state
102
+ return state
103
+
104
+ def _find_script(self) -> Optional[Path]:
105
+ """[Deprecated] No longer used."""
106
+ return None