autoforge-ai 0.1.20 → 0.1.22

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.
@@ -66,10 +66,12 @@ def _get_registry_functions():
66
66
  sys.path.insert(0, str(root))
67
67
 
68
68
  from registry import (
69
+ get_project_auto_improve,
69
70
  get_project_concurrency,
70
71
  get_project_path,
71
72
  list_registered_projects,
72
73
  register_project,
74
+ set_project_auto_improve,
73
75
  set_project_concurrency,
74
76
  unregister_project,
75
77
  validate_project_path,
@@ -82,6 +84,8 @@ def _get_registry_functions():
82
84
  validate_project_path,
83
85
  get_project_concurrency,
84
86
  set_project_concurrency,
87
+ get_project_auto_improve,
88
+ set_project_auto_improve,
85
89
  )
86
90
 
87
91
 
@@ -118,7 +122,7 @@ async def list_projects():
118
122
  _init_imports()
119
123
  assert _check_spec_exists is not None # guaranteed by _init_imports()
120
124
  (_, _, _, list_registered_projects, validate_project_path,
121
- get_project_concurrency, _) = _get_registry_functions()
125
+ get_project_concurrency, _, _, _) = _get_registry_functions()
122
126
 
123
127
  projects = list_registered_projects()
124
128
  result = []
@@ -140,6 +144,10 @@ async def list_projects():
140
144
  has_spec=has_spec,
141
145
  stats=stats,
142
146
  default_concurrency=info.get("default_concurrency", 3),
147
+ auto_improve_enabled=bool(info.get("auto_improve_enabled", False)),
148
+ auto_improve_interval_minutes=int(
149
+ info.get("auto_improve_interval_minutes", 10) or 10
150
+ ),
143
151
  ))
144
152
 
145
153
  return result
@@ -151,7 +159,7 @@ async def create_project(project: ProjectCreate):
151
159
  _init_imports()
152
160
  assert _scaffold_project_prompts is not None # guaranteed by _init_imports()
153
161
  (register_project, _, get_project_path, list_registered_projects,
154
- _, _, _) = _get_registry_functions()
162
+ _, _, _, _, _) = _get_registry_functions()
155
163
 
156
164
  name = validate_project_name(project.name)
157
165
  project_path = Path(project.path).resolve()
@@ -232,7 +240,8 @@ async def get_project(name: str):
232
240
  _init_imports()
233
241
  assert _check_spec_exists is not None # guaranteed by _init_imports()
234
242
  assert _get_project_prompts_dir is not None # guaranteed by _init_imports()
235
- (_, _, get_project_path, _, _, get_project_concurrency, _) = _get_registry_functions()
243
+ (_, _, get_project_path, _, _, get_project_concurrency, _,
244
+ get_project_auto_improve, _) = _get_registry_functions()
236
245
 
237
246
  name = validate_project_name(name)
238
247
  project_dir = get_project_path(name)
@@ -246,6 +255,7 @@ async def get_project(name: str):
246
255
  has_spec = _check_spec_exists(project_dir)
247
256
  stats = get_project_stats(project_dir)
248
257
  prompts_dir = _get_project_prompts_dir(project_dir)
258
+ ai_enabled, ai_interval = get_project_auto_improve(name)
249
259
 
250
260
  return ProjectDetail(
251
261
  name=name,
@@ -254,6 +264,8 @@ async def get_project(name: str):
254
264
  stats=stats,
255
265
  prompts_dir=str(prompts_dir),
256
266
  default_concurrency=get_project_concurrency(name),
267
+ auto_improve_enabled=ai_enabled,
268
+ auto_improve_interval_minutes=ai_interval,
257
269
  )
258
270
 
259
271
 
@@ -267,7 +279,7 @@ async def delete_project(name: str, delete_files: bool = False):
267
279
  delete_files: If True, also delete the project directory and files
268
280
  """
269
281
  _init_imports()
270
- (_, unregister_project, get_project_path, _, _, _, _) = _get_registry_functions()
282
+ (_, unregister_project, get_project_path, _, _, _, _, _, _) = _get_registry_functions()
271
283
 
272
284
  name = validate_project_name(name)
273
285
  project_dir = get_project_path(name)
@@ -304,7 +316,7 @@ async def get_project_prompts(name: str):
304
316
  """Get the content of project prompt files."""
305
317
  _init_imports()
306
318
  assert _get_project_prompts_dir is not None # guaranteed by _init_imports()
307
- (_, _, get_project_path, _, _, _, _) = _get_registry_functions()
319
+ (_, _, get_project_path, _, _, _, _, _, _) = _get_registry_functions()
308
320
 
309
321
  name = validate_project_name(name)
310
322
  project_dir = get_project_path(name)
@@ -338,7 +350,7 @@ async def update_project_prompts(name: str, prompts: ProjectPromptsUpdate):
338
350
  """Update project prompt files."""
339
351
  _init_imports()
340
352
  assert _get_project_prompts_dir is not None # guaranteed by _init_imports()
341
- (_, _, get_project_path, _, _, _, _) = _get_registry_functions()
353
+ (_, _, get_project_path, _, _, _, _, _, _) = _get_registry_functions()
342
354
 
343
355
  name = validate_project_name(name)
344
356
  project_dir = get_project_path(name)
@@ -368,7 +380,7 @@ async def update_project_prompts(name: str, prompts: ProjectPromptsUpdate):
368
380
  async def get_project_stats_endpoint(name: str):
369
381
  """Get current progress statistics for a project."""
370
382
  _init_imports()
371
- (_, _, get_project_path, _, _, _, _) = _get_registry_functions()
383
+ (_, _, get_project_path, _, _, _, _, _, _) = _get_registry_functions()
372
384
 
373
385
  name = validate_project_name(name)
374
386
  project_dir = get_project_path(name)
@@ -395,7 +407,7 @@ async def reset_project(name: str, full_reset: bool = False):
395
407
  Dictionary with list of deleted files and reset type
396
408
  """
397
409
  _init_imports()
398
- (_, _, get_project_path, _, _, _, _) = _get_registry_functions()
410
+ (_, _, get_project_path, _, _, _, _, _, _) = _get_registry_functions()
399
411
 
400
412
  name = validate_project_name(name)
401
413
  project_dir = get_project_path(name)
@@ -487,12 +499,13 @@ async def reset_project(name: str, full_reset: bool = False):
487
499
 
488
500
  @router.patch("/{name}/settings", response_model=ProjectDetail)
489
501
  async def update_project_settings(name: str, settings: ProjectSettingsUpdate):
490
- """Update project-level settings (concurrency, etc.)."""
502
+ """Update project-level settings (concurrency, auto-improve, etc.)."""
491
503
  _init_imports()
492
504
  assert _check_spec_exists is not None # guaranteed by _init_imports()
493
505
  assert _get_project_prompts_dir is not None # guaranteed by _init_imports()
494
506
  (_, _, get_project_path, _, _, get_project_concurrency,
495
- set_project_concurrency) = _get_registry_functions()
507
+ set_project_concurrency, get_project_auto_improve,
508
+ set_project_auto_improve) = _get_registry_functions()
496
509
 
497
510
  name = validate_project_name(name)
498
511
  project_dir = get_project_path(name)
@@ -509,10 +522,43 @@ async def update_project_settings(name: str, settings: ProjectSettingsUpdate):
509
522
  if not success:
510
523
  raise HTTPException(status_code=500, detail="Failed to update concurrency")
511
524
 
525
+ # Update auto-improve config if either field was provided
526
+ auto_improve_touched = (
527
+ settings.auto_improve_enabled is not None
528
+ or settings.auto_improve_interval_minutes is not None
529
+ )
530
+ if auto_improve_touched:
531
+ try:
532
+ success = set_project_auto_improve(
533
+ name,
534
+ enabled=settings.auto_improve_enabled,
535
+ interval_minutes=settings.auto_improve_interval_minutes,
536
+ )
537
+ except ValueError as e:
538
+ raise HTTPException(status_code=400, detail=str(e))
539
+ if not success:
540
+ raise HTTPException(status_code=500, detail="Failed to update auto-improve settings")
541
+
542
+ # Sync the scheduler with the new state.
543
+ from ..services.scheduler_service import get_scheduler
544
+ scheduler = get_scheduler()
545
+ ai_enabled, ai_interval = get_project_auto_improve(name)
546
+ if ai_enabled:
547
+ try:
548
+ await scheduler.register_auto_improve(name, project_dir, ai_interval)
549
+ except Exception as e:
550
+ raise HTTPException(
551
+ status_code=500,
552
+ detail=f"Failed to register auto-improve schedule: {e}",
553
+ )
554
+ else:
555
+ scheduler.remove_auto_improve(name)
556
+
512
557
  # Return updated project details
513
558
  has_spec = _check_spec_exists(project_dir)
514
559
  stats = get_project_stats(project_dir)
515
560
  prompts_dir = _get_project_prompts_dir(project_dir)
561
+ ai_enabled, ai_interval = get_project_auto_improve(name)
516
562
 
517
563
  return ProjectDetail(
518
564
  name=name,
@@ -521,4 +567,6 @@ async def update_project_settings(name: str, settings: ProjectSettingsUpdate):
521
567
  stats=stats,
522
568
  prompts_dir=str(prompts_dir),
523
569
  default_concurrency=get_project_concurrency(name),
570
+ auto_improve_enabled=ai_enabled,
571
+ auto_improve_interval_minutes=ai_interval,
524
572
  )
@@ -26,6 +26,7 @@ from registry import (
26
26
  AVAILABLE_MODELS,
27
27
  DEFAULT_MODEL,
28
28
  get_all_settings,
29
+ get_effort_setting,
29
30
  get_setting,
30
31
  set_setting,
31
32
  )
@@ -95,6 +96,8 @@ def _parse_bool(value: str | None, default: bool = False) -> bool:
95
96
  return value.lower() == "true"
96
97
 
97
98
 
99
+
100
+
98
101
  @router.get("", response_model=SettingsResponse)
99
102
  async def get_settings():
100
103
  """Get current global settings."""
@@ -114,6 +117,7 @@ async def get_settings():
114
117
  playwright_headless=True, # Always headless - embedded browser view replaces desktop windows
115
118
  batch_size=_parse_int(all_settings.get("batch_size"), 3),
116
119
  testing_batch_size=_parse_int(all_settings.get("testing_batch_size"), 3),
120
+ effort=get_effort_setting(),
117
121
  api_provider=api_provider,
118
122
  api_base_url=all_settings.get("api_base_url"),
119
123
  api_has_auth_token=bool(all_settings.get("api_auth_token")),
@@ -142,6 +146,9 @@ async def update_settings(update: SettingsUpdate):
142
146
  if update.testing_batch_size is not None:
143
147
  set_setting("testing_batch_size", str(update.testing_batch_size))
144
148
 
149
+ if update.effort is not None:
150
+ set_setting("effort", update.effort)
151
+
145
152
  # API provider settings
146
153
  if update.api_provider is not None:
147
154
  old_provider = get_setting("api_provider", "claude")
@@ -182,6 +189,7 @@ async def update_settings(update: SettingsUpdate):
182
189
  playwright_headless=True, # Always headless - embedded browser view replaces desktop windows
183
190
  batch_size=_parse_int(all_settings.get("batch_size"), 3),
184
191
  testing_batch_size=_parse_int(all_settings.get("testing_batch_size"), 3),
192
+ effort=get_effort_setting(),
185
193
  api_provider=api_provider,
186
194
  api_base_url=all_settings.get("api_base_url"),
187
195
  api_has_auth_token=bool(all_settings.get("api_auth_token")),
package/server/schemas.py CHANGED
@@ -18,7 +18,7 @@ _root = Path(__file__).parent.parent
18
18
  if str(_root) not in sys.path:
19
19
  sys.path.insert(0, str(_root))
20
20
 
21
- from registry import DEFAULT_MODEL, VALID_MODELS
21
+ from registry import DEFAULT_MODEL, LEGACY_MODEL_MAP, VALID_MODELS
22
22
 
23
23
  # ============================================================================
24
24
  # Project Schemas
@@ -46,6 +46,8 @@ class ProjectSummary(BaseModel):
46
46
  has_spec: bool
47
47
  stats: ProjectStats
48
48
  default_concurrency: int = 3
49
+ auto_improve_enabled: bool = False
50
+ auto_improve_interval_minutes: int = 10
49
51
 
50
52
 
51
53
  class ProjectDetail(BaseModel):
@@ -56,6 +58,8 @@ class ProjectDetail(BaseModel):
56
58
  stats: ProjectStats
57
59
  prompts_dir: str
58
60
  default_concurrency: int = 3
61
+ auto_improve_enabled: bool = False
62
+ auto_improve_interval_minutes: int = 10
59
63
 
60
64
 
61
65
  class ProjectPrompts(BaseModel):
@@ -75,6 +79,8 @@ class ProjectPromptsUpdate(BaseModel):
75
79
  class ProjectSettingsUpdate(BaseModel):
76
80
  """Request schema for updating project-level settings."""
77
81
  default_concurrency: int | None = None
82
+ auto_improve_enabled: bool | None = None
83
+ auto_improve_interval_minutes: int | None = None
78
84
 
79
85
  @field_validator('default_concurrency')
80
86
  @classmethod
@@ -83,6 +89,13 @@ class ProjectSettingsUpdate(BaseModel):
83
89
  raise ValueError("default_concurrency must be between 1 and 5")
84
90
  return v
85
91
 
92
+ @field_validator('auto_improve_interval_minutes')
93
+ @classmethod
94
+ def validate_auto_improve_interval(cls, v: int | None) -> int | None:
95
+ if v is not None and (v < 1 or v > 1440):
96
+ raise ValueError("auto_improve_interval_minutes must be between 1 and 1440")
97
+ return v
98
+
86
99
 
87
100
  # ============================================================================
88
101
  # Feature Schemas
@@ -471,6 +484,7 @@ class SettingsResponse(BaseModel):
471
484
  playwright_headless: bool = True
472
485
  batch_size: int = 3 # Features per coding agent batch (1-15)
473
486
  testing_batch_size: int = 3 # Features per testing agent batch (1-15)
487
+ effort: Literal["low", "medium", "high", "xhigh", "max"] = "xhigh"
474
488
  api_provider: str = "claude"
475
489
  api_base_url: str | None = None
476
490
  api_has_auth_token: bool = False # Never expose actual token
@@ -491,6 +505,7 @@ class SettingsUpdate(BaseModel):
491
505
  playwright_headless: bool | None = None
492
506
  batch_size: int | None = None # Features per agent batch (1-15)
493
507
  testing_batch_size: int | None = None # Features per testing agent batch (1-15)
508
+ effort: Literal["low", "medium", "high", "xhigh", "max"] | None = None
494
509
  api_provider: str | None = None
495
510
  api_base_url: str | None = Field(None, max_length=500)
496
511
  api_auth_token: str | None = Field(None, max_length=500) # Write-only, never returned
@@ -507,12 +522,16 @@ class SettingsUpdate(BaseModel):
507
522
 
508
523
  @field_validator('model')
509
524
  @classmethod
510
- def validate_model(cls, v: str | None, info) -> str | None: # type: ignore[override]
525
+ def validate_model(cls, v: str | None, info) -> str | None:
511
526
  if v is not None:
512
527
  # Skip VALID_MODELS check when using an alternative API provider
513
528
  api_provider = info.data.get("api_provider")
514
529
  if api_provider and api_provider != "claude":
515
530
  return v
531
+ # Transparently accept legacy IDs so in-flight clients don't 422
532
+ # during an upgrade window; LEGACY_MODEL_MAP already covers migration.
533
+ if v in LEGACY_MODEL_MAP:
534
+ v = LEGACY_MODEL_MAP[v]
516
535
  if v not in VALID_MODELS:
517
536
  raise ValueError(f"Invalid model. Must be one of: {VALID_MODELS}")
518
537
  return v
@@ -270,8 +270,9 @@ class AssistantChatSession:
270
270
  system_cli = shutil.which("claude")
271
271
 
272
272
  # Build environment overrides for API configuration
273
- from registry import DEFAULT_MODEL, get_effective_sdk_env
273
+ from registry import DEFAULT_MODEL, get_effective_sdk_env, get_effort_setting
274
274
  sdk_env = get_effective_sdk_env()
275
+ effort = get_effort_setting()
275
276
 
276
277
  # Determine model from SDK env (provider-aware) or fallback to env/default
277
278
  model = sdk_env.get("ANTHROPIC_DEFAULT_OPUS_MODEL") or os.getenv("ANTHROPIC_DEFAULT_OPUS_MODEL", DEFAULT_MODEL)
@@ -281,6 +282,7 @@ class AssistantChatSession:
281
282
  self.client = ClaudeSDKClient(
282
283
  options=ClaudeAgentOptions(
283
284
  model=model,
285
+ effort=effort, # type: ignore[arg-type] # SDK 0.1.61 Literal omits "xhigh"
284
286
  cli_path=system_cli,
285
287
  # System prompt loaded from CLAUDE.md via setting_sources
286
288
  # This avoids Windows command line length limit (~8191 chars)