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.
- package/.claude/templates/auto_improve_prompt.template.md +160 -0
- package/agent.py +14 -2
- package/autonomous_agent_demo.py +27 -1
- package/client.py +7 -2
- package/package.json +1 -1
- package/prompts.py +24 -0
- package/registry.py +143 -15
- package/server/routers/projects.py +58 -10
- package/server/routers/settings.py +8 -0
- package/server/schemas.py +21 -2
- package/server/services/assistant_chat_session.py +3 -1
- package/server/services/browser_view_service.py +280 -280
- package/server/services/expand_chat_session.py +3 -1
- package/server/services/process_manager.py +17 -0
- package/server/services/scheduler_service.py +154 -4
- package/server/services/spec_chat_session.py +3 -1
- package/ui/dist/assets/index-BvNxzjlP.js +96 -0
- package/ui/dist/assets/index-hSFqqmJF.css +1 -0
- package/ui/dist/assets/{vendor-flow-CSXy01ye.js → vendor-flow-4rkFkfFX.js} +1 -1
- package/ui/dist/assets/vendor-radix-CXoPacKb.js +45 -0
- package/ui/dist/assets/vendor-utils-D_WdX4_S.js +2 -0
- package/ui/dist/index.html +5 -5
- package/ui/dist/assets/index-B5-x5jW8.js +0 -96
- package/ui/dist/assets/index-DkJ1zszK.css +0 -1
- package/ui/dist/assets/vendor-radix-DIVIznMB.js +0 -45
- package/ui/dist/assets/vendor-utils-CnTXttNm.js +0 -2
|
@@ -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, _
|
|
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
|
|
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:
|
|
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)
|