monoco-toolkit 0.2.8__py3-none-any.whl → 0.3.1__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 (63) hide show
  1. monoco/cli/project.py +35 -31
  2. monoco/cli/workspace.py +26 -16
  3. monoco/core/agent/__init__.py +0 -2
  4. monoco/core/agent/action.py +44 -20
  5. monoco/core/agent/adapters.py +20 -16
  6. monoco/core/agent/protocol.py +5 -4
  7. monoco/core/agent/state.py +21 -21
  8. monoco/core/config.py +90 -33
  9. monoco/core/execution.py +21 -16
  10. monoco/core/feature.py +8 -5
  11. monoco/core/git.py +61 -30
  12. monoco/core/hooks.py +57 -0
  13. monoco/core/injection.py +47 -44
  14. monoco/core/integrations.py +50 -35
  15. monoco/core/lsp.py +12 -1
  16. monoco/core/output.py +35 -16
  17. monoco/core/registry.py +3 -2
  18. monoco/core/setup.py +190 -124
  19. monoco/core/skills.py +121 -107
  20. monoco/core/state.py +12 -10
  21. monoco/core/sync.py +85 -56
  22. monoco/core/telemetry.py +10 -6
  23. monoco/core/workspace.py +26 -19
  24. monoco/daemon/app.py +123 -79
  25. monoco/daemon/commands.py +14 -13
  26. monoco/daemon/models.py +11 -3
  27. monoco/daemon/reproduce_stats.py +8 -8
  28. monoco/daemon/services.py +32 -33
  29. monoco/daemon/stats.py +59 -40
  30. monoco/features/config/commands.py +38 -25
  31. monoco/features/i18n/adapter.py +4 -5
  32. monoco/features/i18n/commands.py +83 -49
  33. monoco/features/i18n/core.py +94 -54
  34. monoco/features/issue/adapter.py +6 -7
  35. monoco/features/issue/commands.py +468 -272
  36. monoco/features/issue/core.py +419 -312
  37. monoco/features/issue/domain/lifecycle.py +33 -23
  38. monoco/features/issue/domain/models.py +71 -38
  39. monoco/features/issue/domain/parser.py +92 -69
  40. monoco/features/issue/domain/workspace.py +19 -16
  41. monoco/features/issue/engine/__init__.py +3 -3
  42. monoco/features/issue/engine/config.py +18 -25
  43. monoco/features/issue/engine/machine.py +72 -39
  44. monoco/features/issue/engine/models.py +4 -2
  45. monoco/features/issue/linter.py +287 -157
  46. monoco/features/issue/lsp/definition.py +26 -19
  47. monoco/features/issue/migration.py +45 -34
  48. monoco/features/issue/models.py +29 -13
  49. monoco/features/issue/monitor.py +24 -8
  50. monoco/features/issue/resources/en/SKILL.md +6 -2
  51. monoco/features/issue/validator.py +395 -208
  52. monoco/features/skills/__init__.py +0 -1
  53. monoco/features/skills/core.py +24 -18
  54. monoco/features/spike/adapter.py +4 -5
  55. monoco/features/spike/commands.py +51 -38
  56. monoco/features/spike/core.py +24 -16
  57. monoco/main.py +34 -21
  58. {monoco_toolkit-0.2.8.dist-info → monoco_toolkit-0.3.1.dist-info}/METADATA +1 -1
  59. monoco_toolkit-0.3.1.dist-info/RECORD +84 -0
  60. monoco_toolkit-0.2.8.dist-info/RECORD +0 -83
  61. {monoco_toolkit-0.2.8.dist-info → monoco_toolkit-0.3.1.dist-info}/WHEEL +0 -0
  62. {monoco_toolkit-0.2.8.dist-info → monoco_toolkit-0.3.1.dist-info}/entry_points.txt +0 -0
  63. {monoco_toolkit-0.2.8.dist-info → monoco_toolkit-0.3.1.dist-info}/licenses/LICENSE +0 -0
monoco/core/skills.py CHANGED
@@ -14,7 +14,7 @@ Key Responsibilities:
14
14
  import shutil
15
15
  import hashlib
16
16
  from pathlib import Path
17
- from typing import Dict, List, Optional, Set
17
+ from typing import Dict, List, Optional
18
18
  from pydantic import BaseModel, Field, ValidationError
19
19
  from rich.console import Console
20
20
  import yaml
@@ -27,21 +27,27 @@ class SkillMetadata(BaseModel):
27
27
  Skill metadata from YAML frontmatter.
28
28
  Based on agentskills.io standard.
29
29
  """
30
+
30
31
  name: str = Field(..., description="Unique skill identifier (lowercase, hyphens)")
31
- description: str = Field(..., description="Clear description of what the skill does and when to use it")
32
+ description: str = Field(
33
+ ..., description="Clear description of what the skill does and when to use it"
34
+ )
32
35
  version: Optional[str] = Field(default=None, description="Skill version")
33
36
  author: Optional[str] = Field(default=None, description="Skill author")
34
- tags: Optional[List[str]] = Field(default=None, description="Skill tags for categorization")
37
+ tags: Optional[List[str]] = Field(
38
+ default=None, description="Skill tags for categorization"
39
+ )
35
40
 
36
41
 
37
42
  class Skill:
38
43
  """
39
44
  Represents a single skill with its metadata and file paths.
40
45
  """
46
+
41
47
  def __init__(self, root_dir: Path, skill_dir: Path):
42
48
  """
43
49
  Initialize a Skill instance.
44
-
50
+
45
51
  Args:
46
52
  root_dir: Project root directory
47
53
  skill_dir: Path to the skill directory (e.g., Toolkit/skills/issues-management)
@@ -52,13 +58,13 @@ class Skill:
52
58
  self.skill_file = skill_dir / "SKILL.md"
53
59
  self.metadata: Optional[SkillMetadata] = None
54
60
  self._load_metadata()
55
-
61
+
56
62
  def _load_metadata(self) -> None:
57
63
  """Load and validate skill metadata from SKILL.md frontmatter."""
58
64
  # Try to load from language subdirectories first (Feature resources pattern)
59
65
  # Then fallback to root SKILL.md (legacy pattern)
60
66
  skill_file_to_use = None
61
-
67
+
62
68
  # Check language subdirectories
63
69
  if self.skill_dir.exists():
64
70
  for item in sorted(self.skill_dir.iterdir()):
@@ -67,14 +73,14 @@ class Skill:
67
73
  if candidate.exists():
68
74
  skill_file_to_use = candidate
69
75
  break
70
-
76
+
71
77
  # Fallback to root SKILL.md
72
78
  if not skill_file_to_use and self.skill_file.exists():
73
79
  skill_file_to_use = self.skill_file
74
-
80
+
75
81
  if not skill_file_to_use:
76
82
  return
77
-
83
+
78
84
  try:
79
85
  content = skill_file_to_use.read_text(encoding="utf-8")
80
86
  # Extract YAML frontmatter
@@ -83,27 +89,29 @@ class Skill:
83
89
  if len(parts) >= 3:
84
90
  frontmatter = parts[1].strip()
85
91
  metadata_dict = yaml.safe_load(frontmatter)
86
-
92
+
87
93
  # Validate against schema
88
94
  self.metadata = SkillMetadata(**metadata_dict)
89
95
  except ValidationError as e:
90
96
  console.print(f"[red]Invalid metadata in {skill_file_to_use}: {e}[/red]")
91
97
  except Exception as e:
92
- console.print(f"[yellow]Warning: Failed to parse metadata from {skill_file_to_use}: {e}[/yellow]")
93
-
98
+ console.print(
99
+ f"[yellow]Warning: Failed to parse metadata from {skill_file_to_use}: {e}[/yellow]"
100
+ )
101
+
94
102
  def is_valid(self) -> bool:
95
103
  """Check if the skill has valid metadata."""
96
104
  return self.metadata is not None
97
-
105
+
98
106
  def get_languages(self) -> List[str]:
99
107
  """
100
108
  Detect available language versions of this skill.
101
-
109
+
102
110
  Returns:
103
111
  List of language codes (e.g., ['en', 'zh'])
104
112
  """
105
113
  languages = []
106
-
114
+
107
115
  # Check for language subdirectories (Feature resources pattern)
108
116
  # resources/en/SKILL.md, resources/zh/SKILL.md
109
117
  for item in self.skill_dir.iterdir():
@@ -111,36 +119,36 @@ class Skill:
111
119
  lang_skill_file = item / "SKILL.md"
112
120
  if lang_skill_file.exists():
113
121
  languages.append(item.name)
114
-
122
+
115
123
  # Fallback: check for root SKILL.md (legacy Toolkit/skills pattern)
116
124
  # We don't assume a default language, just return what we found
117
125
  if not languages and self.skill_file.exists():
118
126
  # For legacy pattern, we can't determine the language from structure
119
127
  # Return empty to indicate this skill uses legacy pattern
120
128
  pass
121
-
129
+
122
130
  return languages
123
-
131
+
124
132
  def get_checksum(self, lang: str) -> str:
125
133
  """
126
134
  Calculate checksum for the skill content.
127
-
135
+
128
136
  Args:
129
137
  lang: Language code
130
-
138
+
131
139
  Returns:
132
140
  SHA256 checksum of the skill file
133
141
  """
134
142
  # Try language subdirectory first (Feature resources pattern)
135
143
  target_file = self.skill_dir / lang / "SKILL.md"
136
-
144
+
137
145
  # Fallback to root SKILL.md (legacy pattern)
138
146
  if not target_file.exists():
139
147
  target_file = self.skill_file
140
-
148
+
141
149
  if not target_file.exists():
142
150
  return ""
143
-
151
+
144
152
  content = target_file.read_bytes()
145
153
  return hashlib.sha256(content).hexdigest()
146
154
 
@@ -148,17 +156,17 @@ class Skill:
148
156
  class SkillManager:
149
157
  """
150
158
  Central manager for Monoco skills.
151
-
159
+
152
160
  Responsibilities:
153
161
  - Collect skills from Feature resources
154
162
  - Validate skill structure
155
163
  - Distribute skills to agent framework directories
156
164
  """
157
-
165
+
158
166
  def __init__(self, root: Path, features: Optional[List] = None):
159
167
  """
160
168
  Initialize SkillManager.
161
-
169
+
162
170
  Args:
163
171
  root: Project root directory
164
172
  features: List of MonocoFeature instances (if None, will load from registry)
@@ -166,178 +174,183 @@ class SkillManager:
166
174
  self.root = root
167
175
  self.features = features or []
168
176
  self.skills: Dict[str, Skill] = {}
169
-
177
+
170
178
  if self.features:
171
179
  self._discover_skills_from_features()
172
-
180
+
173
181
  # Also discover core skill (monoco/core/resources/)
174
182
  self._discover_core_skill()
175
-
183
+
176
184
  def _discover_core_skill(self) -> None:
177
185
  """
178
186
  Discover skill from monoco/core/resources/.
179
-
187
+
180
188
  Core is special - it's not a Feature but still has a skill.
181
189
  """
182
190
  core_resources_dir = self.root / "monoco" / "core" / "resources"
183
-
191
+
184
192
  if not core_resources_dir.exists():
185
193
  return
186
-
194
+
187
195
  # Check for SKILL.md in language directories
188
196
  for lang_dir in core_resources_dir.iterdir():
189
197
  if lang_dir.is_dir() and (lang_dir / "SKILL.md").exists():
190
198
  skill = Skill(self.root, core_resources_dir)
191
-
199
+
192
200
  # Use the skill's metadata name if available
193
201
  if skill.metadata and skill.metadata.name:
194
- skill.name = skill.metadata.name.replace('-', '_')
202
+ skill.name = skill.metadata.name.replace("-", "_")
195
203
  else:
196
204
  skill.name = "monoco_core"
197
-
205
+
198
206
  if skill.is_valid():
199
207
  self.skills[skill.name] = skill
200
208
  break # Only need to detect once
201
-
209
+
202
210
  def _discover_skills_from_features(self) -> None:
203
211
  """
204
212
  Discover skills from Feature resources.
205
-
213
+
206
214
  Each feature should have:
207
215
  - monoco/features/{feature}/resources/{lang}/SKILL.md
208
216
  """
209
217
  from monoco.core.feature import MonocoFeature
210
-
218
+
211
219
  for feature in self.features:
212
220
  if not isinstance(feature, MonocoFeature):
213
221
  continue
214
-
222
+
215
223
  # Determine feature module path
216
224
  # feature.__class__.__module__ is like 'monoco.features.issue.adapter'
217
- module_parts = feature.__class__.__module__.split('.')
218
- if len(module_parts) >= 3 and module_parts[0] == 'monoco' and module_parts[1] == 'features':
225
+ module_parts = feature.__class__.__module__.split(".")
226
+ if (
227
+ len(module_parts) >= 3
228
+ and module_parts[0] == "monoco"
229
+ and module_parts[1] == "features"
230
+ ):
219
231
  feature_name = module_parts[2]
220
-
232
+
221
233
  # Construct path to feature resources
222
234
  # monoco/features/{feature}/resources/
223
235
  feature_dir = self.root / "monoco" / "features" / feature_name
224
236
  resources_dir = feature_dir / "resources"
225
-
237
+
226
238
  if not resources_dir.exists():
227
239
  continue
228
-
240
+
229
241
  # Check for SKILL.md in language directories
230
242
  for lang_dir in resources_dir.iterdir():
231
243
  if lang_dir.is_dir() and (lang_dir / "SKILL.md").exists():
232
244
  # Create a Skill instance
233
245
  # We need to adapt the Skill class to work with feature resources
234
- skill = self._create_skill_from_feature(feature_name, resources_dir)
246
+ skill = self._create_skill_from_feature(
247
+ feature_name, resources_dir
248
+ )
235
249
  if skill and skill.is_valid():
236
250
  # Use feature name as skill identifier
237
251
  skill_key = f"{feature_name}"
238
252
  if skill_key not in self.skills:
239
253
  self.skills[skill_key] = skill
240
254
  break # Only need to detect once per feature
241
-
242
- def _create_skill_from_feature(self, feature_name: str, resources_dir: Path) -> Optional[Skill]:
255
+
256
+ def _create_skill_from_feature(
257
+ self, feature_name: str, resources_dir: Path
258
+ ) -> Optional[Skill]:
243
259
  """
244
260
  Create a Skill instance from a feature's resources directory.
245
-
261
+
246
262
  Args:
247
263
  feature_name: Name of the feature (e.g., 'issue', 'spike')
248
264
  resources_dir: Path to the feature's resources directory
249
-
265
+
250
266
  Returns:
251
267
  Skill instance or None if creation fails
252
268
  """
253
269
  # Use the resources directory as the skill directory
254
270
  # The Skill class expects a directory with SKILL.md or {lang}/SKILL.md
255
271
  skill = Skill(self.root, resources_dir)
256
-
272
+
257
273
  # Use the skill's metadata name if available (e.g., 'monoco-issue')
258
274
  # Convert to snake_case for directory name (e.g., 'monoco_issue')
259
275
  if skill.metadata and skill.metadata.name:
260
276
  # Convert kebab-case to snake_case for directory name
261
- skill.name = skill.metadata.name.replace('-', '_')
277
+ skill.name = skill.metadata.name.replace("-", "_")
262
278
  else:
263
279
  # Fallback to feature name
264
280
  skill.name = f"monoco_{feature_name}"
265
-
281
+
266
282
  return skill
267
-
283
+
268
284
  def list_skills(self) -> List[Skill]:
269
285
  """
270
286
  Get all available skills.
271
-
287
+
272
288
  Returns:
273
289
  List of Skill instances
274
290
  """
275
291
  return list(self.skills.values())
276
-
292
+
277
293
  def get_skill(self, name: str) -> Optional[Skill]:
278
294
  """
279
295
  Get a specific skill by name.
280
-
296
+
281
297
  Args:
282
298
  name: Skill name
283
-
299
+
284
300
  Returns:
285
301
  Skill instance or None if not found
286
302
  """
287
303
  return self.skills.get(name)
288
-
304
+
289
305
  def distribute(
290
- self,
291
- target_dir: Path,
292
- lang: str,
293
- force: bool = False
306
+ self, target_dir: Path, lang: str, force: bool = False
294
307
  ) -> Dict[str, bool]:
295
308
  """
296
309
  Distribute skills to a target directory.
297
-
310
+
298
311
  Args:
299
312
  target_dir: Target directory for skill distribution (e.g., .cursor/skills/)
300
313
  lang: Language code to distribute (e.g., 'en', 'zh')
301
314
  force: Force overwrite even if checksum matches
302
-
315
+
303
316
  Returns:
304
317
  Dictionary mapping skill names to success status
305
318
  """
306
319
  results = {}
307
-
320
+
308
321
  # Ensure target directory exists
309
322
  target_dir.mkdir(parents=True, exist_ok=True)
310
-
323
+
311
324
  for skill_name, skill in self.skills.items():
312
325
  try:
313
326
  # Check if this skill has the requested language
314
327
  available_languages = skill.get_languages()
315
-
328
+
316
329
  if lang not in available_languages:
317
- console.print(f"[yellow]Skill {skill_name} does not have {lang} version, skipping[/yellow]")
330
+ console.print(
331
+ f"[yellow]Skill {skill_name} does not have {lang} version, skipping[/yellow]"
332
+ )
318
333
  results[skill_name] = False
319
334
  continue
320
-
335
+
321
336
  # Distribute the specific language version
322
337
  self._distribute_skill_language(skill, target_dir, lang, force)
323
338
  results[skill_name] = True
324
-
339
+
325
340
  except Exception as e:
326
- console.print(f"[red]Failed to distribute skill {skill_name}: {e}[/red]")
341
+ console.print(
342
+ f"[red]Failed to distribute skill {skill_name}: {e}[/red]"
343
+ )
327
344
  results[skill_name] = False
328
-
345
+
329
346
  return results
330
-
347
+
331
348
  def _distribute_skill_language(
332
- self,
333
- skill: Skill,
334
- target_dir: Path,
335
- lang: str,
336
- force: bool
349
+ self, skill: Skill, target_dir: Path, lang: str, force: bool
337
350
  ) -> None:
338
351
  """
339
352
  Distribute a specific language version of a skill.
340
-
353
+
341
354
  Args:
342
355
  skill: Skill instance
343
356
  target_dir: Target directory (e.g., .cursor/skills/)
@@ -346,99 +359,100 @@ class SkillManager:
346
359
  """
347
360
  # Determine source file (try language subdirectory first)
348
361
  source_file = skill.skill_dir / lang / "SKILL.md"
349
-
362
+
350
363
  # Fallback to root SKILL.md (legacy pattern)
351
364
  if not source_file.exists():
352
365
  source_file = skill.skill_file
353
-
366
+
354
367
  if not source_file.exists():
355
- console.print(f"[yellow]Source file not found for {skill.name}/{lang}[/yellow]")
368
+ console.print(
369
+ f"[yellow]Source file not found for {skill.name}/{lang}[/yellow]"
370
+ )
356
371
  return
357
-
372
+
358
373
  # Target path: {target_dir}/{skill_name}/SKILL.md (no language subdirectory)
359
374
  target_skill_dir = target_dir / skill.name
360
-
375
+
361
376
  # Create target directory
362
377
  target_skill_dir.mkdir(parents=True, exist_ok=True)
363
378
  target_file = target_skill_dir / "SKILL.md"
364
-
379
+
365
380
  # Check if update is needed
366
381
  if target_file.exists() and not force:
367
382
  source_checksum = skill.get_checksum(lang)
368
383
  target_content = target_file.read_bytes()
369
384
  target_checksum = hashlib.sha256(target_content).hexdigest()
370
-
385
+
371
386
  if source_checksum == target_checksum:
372
387
  console.print(f"[dim] = {skill.name}/SKILL.md is up to date[/dim]")
373
388
  return
374
-
389
+
375
390
  # Copy the file
376
391
  shutil.copy2(source_file, target_file)
377
392
  console.print(f"[green] ✓ Distributed {skill.name}/SKILL.md ({lang})[/green]")
378
-
393
+
379
394
  # Copy additional resources if they exist
380
395
  self._copy_skill_resources(skill.skill_dir, target_skill_dir, lang)
381
-
396
+
382
397
  def _copy_skill_resources(
383
- self,
384
- source_dir: Path,
385
- target_dir: Path,
386
- lang: str
398
+ self, source_dir: Path, target_dir: Path, lang: str
387
399
  ) -> None:
388
400
  """
389
401
  Copy additional skill resources (scripts, examples, etc.).
390
-
402
+
391
403
  Args:
392
404
  source_dir: Source skill directory
393
405
  target_dir: Target skill directory
394
406
  lang: Language code
395
407
  """
396
408
  # Define resource directories to copy
397
- resource_dirs = ['scripts', 'examples', 'resources']
398
-
409
+ resource_dirs = ["scripts", "examples", "resources"]
410
+
399
411
  # Try language subdirectory first (Feature resources pattern)
400
412
  source_base = source_dir / lang
401
-
413
+
402
414
  # Fallback to root directory (legacy pattern)
403
415
  if not source_base.exists():
404
416
  source_base = source_dir
405
-
417
+
406
418
  for resource_name in resource_dirs:
407
419
  source_resource = source_base / resource_name
408
420
  if source_resource.exists() and source_resource.is_dir():
409
421
  target_resource = target_dir / resource_name
410
-
422
+
411
423
  # Remove existing and copy fresh
412
424
  if target_resource.exists():
413
425
  shutil.rmtree(target_resource)
414
-
426
+
415
427
  shutil.copytree(source_resource, target_resource)
416
- console.print(f"[dim] Copied {resource_name}/ for {source_dir.name}/{lang}[/dim]")
417
-
428
+ console.print(
429
+ f"[dim] Copied {resource_name}/ for {source_dir.name}/{lang}[/dim]"
430
+ )
431
+
418
432
  def cleanup(self, target_dir: Path) -> None:
419
433
  """
420
434
  Remove distributed skills from a target directory.
421
-
435
+
422
436
  Args:
423
437
  target_dir: Target directory to clean
424
438
  """
425
439
  if not target_dir.exists():
426
440
  console.print(f"[dim]Target directory does not exist: {target_dir}[/dim]")
427
441
  return
428
-
442
+
429
443
  removed_count = 0
430
-
444
+
431
445
  for skill_name in self.skills.keys():
432
446
  skill_target = target_dir / skill_name
433
447
  if skill_target.exists():
434
448
  shutil.rmtree(skill_target)
435
449
  console.print(f"[green] ✓ Removed {skill_name}[/green]")
436
450
  removed_count += 1
437
-
451
+
438
452
  # Remove empty parent directory if no skills remain
439
453
  if target_dir.exists() and not any(target_dir.iterdir()):
440
454
  target_dir.rmdir()
441
455
  console.print(f"[dim] Removed empty directory: {target_dir}[/dim]")
442
-
456
+
443
457
  if removed_count == 0:
444
458
  console.print(f"[dim]No skills to remove from {target_dir}[/dim]")
monoco/core/state.py CHANGED
@@ -1,26 +1,28 @@
1
1
  from pathlib import Path
2
- from typing import Optional, Any, Dict
2
+ from typing import Optional
3
3
  import json
4
4
  import logging
5
- from pydantic import BaseModel, Field
5
+ from pydantic import BaseModel
6
6
 
7
7
  logger = logging.getLogger("monoco.core.state")
8
8
 
9
+
9
10
  class WorkspaceState(BaseModel):
10
11
  """
11
12
  Persisted state for a Monoco workspace (collection of projects).
12
13
  Stored in <workspace_root>/.monoco/state.json
13
14
  """
15
+
14
16
  last_active_project_id: Optional[str] = None
15
-
17
+
16
18
  @classmethod
17
19
  def load(cls, workspace_root: Path) -> "WorkspaceState":
18
20
  state_file = workspace_root / ".monoco" / "state.json"
19
21
  if not state_file.exists():
20
22
  return cls()
21
-
23
+
22
24
  try:
23
- content = state_file.read_text(encoding='utf-8')
25
+ content = state_file.read_text(encoding="utf-8")
24
26
  if not content.strip():
25
27
  return cls()
26
28
  data = json.loads(content)
@@ -32,22 +34,22 @@ class WorkspaceState(BaseModel):
32
34
  def save(self, workspace_root: Path):
33
35
  state_file = workspace_root / ".monoco" / "state.json"
34
36
  state_file.parent.mkdir(parents=True, exist_ok=True)
35
-
37
+
36
38
  try:
37
39
  # We merge with existing on disk if possible to preserve unknown keys
38
40
  current_data = {}
39
41
  if state_file.exists():
40
42
  try:
41
- content = state_file.read_text(encoding='utf-8')
43
+ content = state_file.read_text(encoding="utf-8")
42
44
  if content.strip():
43
45
  current_data = json.loads(content)
44
46
  except:
45
47
  pass
46
-
48
+
47
49
  new_data = self.model_dump(exclude_unset=True)
48
50
  current_data.update(new_data)
49
-
50
- state_file.write_text(json.dumps(current_data, indent=2), encoding='utf-8')
51
+
52
+ state_file.write_text(json.dumps(current_data, indent=2), encoding="utf-8")
51
53
  except Exception as e:
52
54
  logger.error(f"Failed to save workspace state to {state_file}: {e}")
53
55
  raise