autoforge-ai 0.1.21 → 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/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-utils-_RSkPk2f.js → vendor-utils-D_WdX4_S.js} +1 -1
- package/ui/dist/index.html +3 -3
- package/ui/dist/assets/index-BB9FkE5a.js +0 -96
- package/ui/dist/assets/index-CaH_F11g.css +0 -1
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
## YOUR ROLE - AUTO-IMPROVE AGENT
|
|
2
|
+
|
|
3
|
+
You are running in **auto-improve mode**. Your entire job this session is to make the application **meaningfully better** in exactly ONE way. The project is already finished — all existing features pass. You are here to polish, enhance, and evolve it.
|
|
4
|
+
|
|
5
|
+
This is a FRESH context window. You have no memory of previous sessions. Previous auto-improve sessions may have already added improvements. Your job is to pick ONE new improvement, implement it, and commit it.
|
|
6
|
+
|
|
7
|
+
### STEP 1: GET YOUR BEARINGS
|
|
8
|
+
|
|
9
|
+
Start by orienting yourself:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Understand the project
|
|
13
|
+
pwd
|
|
14
|
+
ls -la
|
|
15
|
+
cat app_spec.txt 2>/dev/null || cat .autoforge/prompts/app_spec.txt 2>/dev/null
|
|
16
|
+
|
|
17
|
+
# See what's been done recently (previous auto-improvements, other commits)
|
|
18
|
+
git log --oneline -20
|
|
19
|
+
|
|
20
|
+
# See recent progress notes if they exist
|
|
21
|
+
tail -200 claude-progress.txt 2>/dev/null || true
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Then use MCP tools to check feature status:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
Use the feature_get_stats tool
|
|
28
|
+
Use the feature_get_summary tool
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
You are looking at an app that someone is running in "autopilot polish" mode. Respect what is already there. Read some of the actual source to get a feel for the codebase.
|
|
32
|
+
|
|
33
|
+
### STEP 2: CHOOSE ONE MEANINGFUL IMPROVEMENT
|
|
34
|
+
|
|
35
|
+
Brainstorm silently, then pick exactly ONE improvement. Valid categories:
|
|
36
|
+
|
|
37
|
+
- **Performance** — cache a hot path, remove an N+1, memoize an expensive component, debounce a noisy handler
|
|
38
|
+
- **UX / UI polish** — empty states, loading states, error states, keyboard shortcuts, micro-interactions, accessibility
|
|
39
|
+
- **Visual design** — spacing, typography, color hierarchy, alignment, iconography
|
|
40
|
+
- **Small new feature** — a natural next step that fits the app's purpose
|
|
41
|
+
- **Security hardening** — input validation, authorization checks, rate limits, secret handling
|
|
42
|
+
- **Refactor for clarity** — extract a confused function, rename a misleading variable, split a file that has outgrown itself
|
|
43
|
+
- **Accessibility** — focus rings, aria-labels, keyboard navigation, color contrast
|
|
44
|
+
- **Dependency / config** — bump a safe dep, tighten a lint rule that would catch a real class of bugs
|
|
45
|
+
|
|
46
|
+
**Choose deliberately:**
|
|
47
|
+
- The improvement must be genuinely useful to an end user or to future developers.
|
|
48
|
+
- Prefer improvements that complement what's already there over inventing new scope.
|
|
49
|
+
- If the app has obvious rough edges, fix those first before inventing new features.
|
|
50
|
+
- Do NOT touch any feature on the Kanban that is currently `in_progress` — leave it alone.
|
|
51
|
+
- Avoid duplicating past improvements (read `git log` to see what's already been done).
|
|
52
|
+
|
|
53
|
+
### STEP 3: ADD THE IMPROVEMENT AS A FEATURE
|
|
54
|
+
|
|
55
|
+
Call the `feature_create` MCP tool with:
|
|
56
|
+
|
|
57
|
+
- `category`: e.g., `"Performance"`, `"UX Polish"`, `"Security"`, `"Refactor"`, `"Accessibility"`, `"New Feature"`
|
|
58
|
+
- `name`: a short imperative title, e.g., `"Add empty state to project list"`
|
|
59
|
+
- `description`: 1-3 sentences explaining what the change is and why it matters
|
|
60
|
+
- `steps`: 3-5 concrete acceptance steps (what must be true when this is done)
|
|
61
|
+
|
|
62
|
+
**Record the returned feature ID.** You will use it in later steps. Then mark it in progress:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
Use the feature_mark_in_progress tool with feature_id={your_new_id}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### STEP 4: IMPLEMENT THE IMPROVEMENT
|
|
69
|
+
|
|
70
|
+
Implement the change fully. Keep scope tight:
|
|
71
|
+
|
|
72
|
+
- Edit only the files you need to change.
|
|
73
|
+
- Don't add speculative abstractions or "while I'm here" refactors.
|
|
74
|
+
- Don't add comments/docstrings to code you didn't touch.
|
|
75
|
+
- Don't rename things that don't need renaming.
|
|
76
|
+
- If you discover a bug that is NOT your chosen improvement, leave it alone (or note it in `claude-progress.txt` for a future session).
|
|
77
|
+
|
|
78
|
+
If your improvement is a UI change, actually look at the result — take a screenshot with `playwright-cli` if the dev server is running, or at minimum open the relevant component and verify your edit makes sense.
|
|
79
|
+
|
|
80
|
+
### STEP 5: VERIFY WITH LINT / TYPECHECK / BUILD
|
|
81
|
+
|
|
82
|
+
**Mandatory.** Before committing, confirm the code still compiles cleanly. Pick the right commands based on the project type (check `package.json`, `pyproject.toml`, `Cargo.toml`, etc.).
|
|
83
|
+
|
|
84
|
+
Typical command sets:
|
|
85
|
+
|
|
86
|
+
- **Node / TypeScript / Vite / Next**: `npm run lint && npm run build`
|
|
87
|
+
(or `npm run typecheck` if it exists as a separate script)
|
|
88
|
+
- **Python**: `ruff check . && mypy .` (or whatever is configured in `pyproject.toml`)
|
|
89
|
+
- **Rust**: `cargo check && cargo clippy`
|
|
90
|
+
- **Go**: `go vet ./... && go build ./...`
|
|
91
|
+
|
|
92
|
+
**Resolve any issues your change introduced.** If lint/typecheck/build was already failing before your change (unrelated breakage), do NOT "fix" the unrelated failures — that's scope creep. Revert your change and pick a different improvement if the codebase is in a broken baseline state.
|
|
93
|
+
|
|
94
|
+
### STEP 6: MARK THE FEATURE PASSING
|
|
95
|
+
|
|
96
|
+
Call the feature MCP tool:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
Use the feature_mark_passing tool with feature_id={your_new_id}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### STEP 7: CREATE A COMMIT
|
|
103
|
+
|
|
104
|
+
Stage your changes and commit with a **short, concise, TLDR-style message**. One line for the subject, optionally one or two more for the "why". No verbose bullet lists, no trailing summaries.
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
git status
|
|
108
|
+
git add <specific files you changed>
|
|
109
|
+
git commit -m "Add empty state to project list when no projects exist"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Good commit message examples:
|
|
113
|
+
- `"Cache project stats query to cut dashboard load time"`
|
|
114
|
+
- `"Add keyboard shortcut (Cmd+K) to open command palette"`
|
|
115
|
+
- `"Harden upload endpoint against oversized files"`
|
|
116
|
+
- `"Extract confused session handling into its own module"`
|
|
117
|
+
|
|
118
|
+
Bad commit message examples:
|
|
119
|
+
- `"Various improvements"` (too vague)
|
|
120
|
+
- `"Made the app better by implementing several changes to improve UX including..."` (too long)
|
|
121
|
+
|
|
122
|
+
### STEP 8: EXIT THIS SESSION
|
|
123
|
+
|
|
124
|
+
When the commit is created successfully, your work for this session is done. Do NOT try to find a second improvement — one per session is the rule. Stop and let the next scheduled tick handle the next improvement.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## GUARDRAILS (READ CAREFULLY)
|
|
129
|
+
|
|
130
|
+
1. **One improvement per session.** If you finish early, don't start another. Exit cleanly.
|
|
131
|
+
2. **Never skip lint / typecheck / build.** If they fail, fix or revert.
|
|
132
|
+
3. **Never commit broken code.** A commit with failing lint/build is worse than no commit.
|
|
133
|
+
4. **Don't touch features other agents are working on** (anything with `in_progress=True`).
|
|
134
|
+
5. **Don't bypass the feature MCP tools.** Create a real Kanban feature for your change so it shows up in the UI.
|
|
135
|
+
6. **Keep commit messages under 72 characters for the subject line.**
|
|
136
|
+
7. **Don't add dependencies you don't need.** If the improvement needs a new package, be sure it's justified.
|
|
137
|
+
8. **Respect the existing architecture.** Don't rewrite patterns the project has already committed to.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## BROWSER AUTOMATION (OPTIONAL)
|
|
142
|
+
|
|
143
|
+
If your improvement is visual and the dev server is running, you may use `playwright-cli` to verify it renders correctly:
|
|
144
|
+
|
|
145
|
+
- Open: `playwright-cli open http://localhost:PORT`
|
|
146
|
+
- Screenshot: `playwright-cli screenshot`
|
|
147
|
+
- Read the screenshot file to verify visual appearance
|
|
148
|
+
- Close: `playwright-cli close`
|
|
149
|
+
|
|
150
|
+
Browser verification is **optional** in auto-improve mode. Lint + typecheck + build is mandatory; visual verification is a bonus when relevant.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## SUCCESS CRITERIA
|
|
155
|
+
|
|
156
|
+
A successful auto-improve session ends with:
|
|
157
|
+
1. One new feature on the Kanban, marked passing.
|
|
158
|
+
2. A clean git commit with a short TLDR message.
|
|
159
|
+
3. No lint / typecheck / build errors introduced.
|
|
160
|
+
4. The agent exits cleanly without starting a second improvement.
|
package/agent.py
CHANGED
|
@@ -31,6 +31,7 @@ from progress import (
|
|
|
31
31
|
)
|
|
32
32
|
from prompts import (
|
|
33
33
|
copy_spec_to_project,
|
|
34
|
+
get_auto_improve_prompt,
|
|
34
35
|
get_batch_feature_prompt,
|
|
35
36
|
get_coding_prompt,
|
|
36
37
|
get_initializer_prompt,
|
|
@@ -163,6 +164,7 @@ async def run_autonomous_agent(
|
|
|
163
164
|
agent_type: Optional[str] = None,
|
|
164
165
|
testing_feature_id: Optional[int] = None,
|
|
165
166
|
testing_feature_ids: Optional[list[int]] = None,
|
|
167
|
+
auto_improve: bool = False,
|
|
166
168
|
) -> None:
|
|
167
169
|
"""
|
|
168
170
|
Run the autonomous agent loop.
|
|
@@ -177,6 +179,9 @@ async def run_autonomous_agent(
|
|
|
177
179
|
agent_type: Type of agent: "initializer", "coding", "testing", or None (auto-detect)
|
|
178
180
|
testing_feature_id: For testing agents, the pre-claimed feature ID to test (legacy single mode)
|
|
179
181
|
testing_feature_ids: For testing agents, list of feature IDs to batch test
|
|
182
|
+
auto_improve: If True, run in auto-improve mode (agent creates one
|
|
183
|
+
improvement feature, implements it, commits, and exits). Takes
|
|
184
|
+
precedence over other prompt selection branches.
|
|
180
185
|
"""
|
|
181
186
|
print("\n" + "=" * 70)
|
|
182
187
|
print(" AUTONOMOUS CODING AGENT")
|
|
@@ -185,6 +190,8 @@ async def run_autonomous_agent(
|
|
|
185
190
|
print(f"Model: {model}")
|
|
186
191
|
if agent_type:
|
|
187
192
|
print(f"Agent type: {agent_type}")
|
|
193
|
+
if auto_improve:
|
|
194
|
+
print("Mode: AUTO-IMPROVE (one improvement + commit per session)")
|
|
188
195
|
if yolo_mode:
|
|
189
196
|
print("Mode: YOLO (testing agents disabled)")
|
|
190
197
|
if feature_ids and len(feature_ids) > 1:
|
|
@@ -240,7 +247,8 @@ async def run_autonomous_agent(
|
|
|
240
247
|
|
|
241
248
|
# Check if all features are already complete (before starting a new session)
|
|
242
249
|
# Skip this check if running as initializer (needs to create features first)
|
|
243
|
-
|
|
250
|
+
# or auto-improve mode (intentionally runs against finished projects)
|
|
251
|
+
if not is_initializer and not auto_improve and iteration == 1:
|
|
244
252
|
passing, in_progress, total, _nhi = count_passing_tests(project_dir)
|
|
245
253
|
if total > 0 and passing == total:
|
|
246
254
|
print("\n" + "=" * 70)
|
|
@@ -262,7 +270,11 @@ async def run_autonomous_agent(
|
|
|
262
270
|
client = create_client(project_dir, model, yolo_mode=yolo_mode, agent_type=agent_type)
|
|
263
271
|
|
|
264
272
|
# Choose prompt based on agent type
|
|
265
|
-
|
|
273
|
+
# auto_improve takes precedence over other branches — it's a distinct
|
|
274
|
+
# mode where the agent creates its own feature before implementing it.
|
|
275
|
+
if auto_improve:
|
|
276
|
+
prompt = get_auto_improve_prompt(project_dir, yolo_mode=yolo_mode)
|
|
277
|
+
elif agent_type == "initializer":
|
|
266
278
|
prompt = get_initializer_prompt(project_dir)
|
|
267
279
|
elif agent_type == "testing":
|
|
268
280
|
prompt = get_testing_prompt(project_dir, testing_feature_id, testing_feature_ids)
|
package/autonomous_agent_demo.py
CHANGED
|
@@ -186,6 +186,17 @@ Authentication:
|
|
|
186
186
|
help="Max features per coding agent batch (1-15, default: 3)",
|
|
187
187
|
)
|
|
188
188
|
|
|
189
|
+
parser.add_argument(
|
|
190
|
+
"--auto-improve",
|
|
191
|
+
action="store_true",
|
|
192
|
+
default=False,
|
|
193
|
+
help=(
|
|
194
|
+
"Run in auto-improve mode: a single agent session that analyses "
|
|
195
|
+
"the codebase, creates one improvement feature, implements it, "
|
|
196
|
+
"verifies with lint/typecheck/build, commits, and exits."
|
|
197
|
+
),
|
|
198
|
+
)
|
|
199
|
+
|
|
189
200
|
return parser.parse_args()
|
|
190
201
|
|
|
191
202
|
|
|
@@ -262,7 +273,22 @@ def main() -> None:
|
|
|
262
273
|
return
|
|
263
274
|
|
|
264
275
|
try:
|
|
265
|
-
if args.
|
|
276
|
+
if args.auto_improve:
|
|
277
|
+
# Auto-improve mode: single agent session, one improvement per run.
|
|
278
|
+
# Bypasses the parallel orchestrator entirely — auto-improve is
|
|
279
|
+
# always single-agent, single-feature, and exits after one commit.
|
|
280
|
+
print("[AUTO-IMPROVE] Starting single-session improvement run...", flush=True)
|
|
281
|
+
asyncio.run(
|
|
282
|
+
run_autonomous_agent(
|
|
283
|
+
project_dir=project_dir,
|
|
284
|
+
model=args.model,
|
|
285
|
+
max_iterations=1,
|
|
286
|
+
yolo_mode=args.yolo,
|
|
287
|
+
agent_type="coding",
|
|
288
|
+
auto_improve=True,
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
elif args.agent_type:
|
|
266
292
|
# Subprocess mode - spawned by orchestrator for a specific role
|
|
267
293
|
asyncio.run(
|
|
268
294
|
run_autonomous_agent(
|
package/client.py
CHANGED
|
@@ -38,7 +38,7 @@ def convert_model_for_vertex(model: str) -> str:
|
|
|
38
38
|
|
|
39
39
|
Vertex AI uses @ to separate model name from version (e.g., claude-sonnet-4-5@20250929)
|
|
40
40
|
while the Anthropic API uses - (e.g., claude-sonnet-4-5-20250929).
|
|
41
|
-
Models without a date suffix (e.g., claude-opus-4-
|
|
41
|
+
Models without a date suffix (e.g., claude-opus-4-7) pass through unchanged.
|
|
42
42
|
|
|
43
43
|
Args:
|
|
44
44
|
model: Model name in Anthropic format (with hyphens)
|
|
@@ -342,8 +342,10 @@ def create_client(
|
|
|
342
342
|
# Uses get_effective_sdk_env() which reads provider settings from the database,
|
|
343
343
|
# ensuring UI-configured alternative providers (GLM, Ollama, Kimi, Custom) propagate
|
|
344
344
|
# correctly to the Claude CLI subprocess
|
|
345
|
-
from registry import get_effective_sdk_env
|
|
345
|
+
from registry import get_effective_sdk_env, get_effort_setting
|
|
346
346
|
sdk_env = get_effective_sdk_env()
|
|
347
|
+
effort = get_effort_setting()
|
|
348
|
+
print(f" - Reasoning effort: {effort}")
|
|
347
349
|
|
|
348
350
|
# Detect alternative API mode (Ollama, GLM, or Vertex AI)
|
|
349
351
|
base_url = sdk_env.get("ANTHROPIC_BASE_URL", "")
|
|
@@ -452,6 +454,9 @@ def create_client(
|
|
|
452
454
|
return ClaudeSDKClient(
|
|
453
455
|
options=ClaudeAgentOptions(
|
|
454
456
|
model=model,
|
|
457
|
+
# SDK 0.1.61's effort Literal omits "xhigh" but the CLI's
|
|
458
|
+
# --effort flag accepts it; the SDK forwards the string unchanged.
|
|
459
|
+
effort=effort, # type: ignore[arg-type]
|
|
455
460
|
cli_path=system_cli, # Use system CLI to avoid bundled Bun crash (exit code 3)
|
|
456
461
|
system_prompt="You are an expert full-stack developer building a production-quality web application.",
|
|
457
462
|
setting_sources=["project"], # Enable skills, commands, and CLAUDE.md from project dir
|
package/package.json
CHANGED
package/prompts.py
CHANGED
|
@@ -151,6 +151,30 @@ def get_coding_prompt(project_dir: Path | None = None, yolo_mode: bool = False)
|
|
|
151
151
|
return prompt
|
|
152
152
|
|
|
153
153
|
|
|
154
|
+
def get_auto_improve_prompt(project_dir: Path | None = None, yolo_mode: bool = False) -> str:
|
|
155
|
+
"""Load the auto-improve agent prompt (project-specific if available).
|
|
156
|
+
|
|
157
|
+
The auto-improve prompt instructs the agent to analyze an already-finished
|
|
158
|
+
project, pick ONE meaningful improvement, create a feature on the Kanban,
|
|
159
|
+
implement it, verify with lint/typecheck/build, mark passing, and commit.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
project_dir: Optional project directory for project-specific prompts
|
|
163
|
+
yolo_mode: If True, strip browser automation sections for YOLO-mode
|
|
164
|
+
token savings. Browser verification is already optional in
|
|
165
|
+
auto-improve mode, so this is a small adjustment.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
The auto-improve prompt, optionally stripped of browser testing.
|
|
169
|
+
"""
|
|
170
|
+
prompt = load_prompt("auto_improve_prompt", project_dir)
|
|
171
|
+
|
|
172
|
+
if yolo_mode:
|
|
173
|
+
prompt = _strip_browser_testing_sections(prompt)
|
|
174
|
+
|
|
175
|
+
return prompt
|
|
176
|
+
|
|
177
|
+
|
|
154
178
|
def get_testing_prompt(
|
|
155
179
|
project_dir: Path | None = None,
|
|
156
180
|
testing_feature_id: int | None = None,
|
package/registry.py
CHANGED
|
@@ -14,9 +14,9 @@ import time
|
|
|
14
14
|
from contextlib import contextmanager
|
|
15
15
|
from datetime import datetime
|
|
16
16
|
from pathlib import Path
|
|
17
|
-
from typing import Any
|
|
17
|
+
from typing import Any, Literal, cast
|
|
18
18
|
|
|
19
|
-
from sqlalchemy import Column, DateTime, Integer, String, create_engine, text
|
|
19
|
+
from sqlalchemy import Boolean, Column, DateTime, Integer, String, create_engine, text
|
|
20
20
|
from sqlalchemy.orm import DeclarativeBase, sessionmaker
|
|
21
21
|
|
|
22
22
|
# Module logger
|
|
@@ -46,14 +46,17 @@ def _migrate_registry_dir() -> None:
|
|
|
46
46
|
# Available models with display names
|
|
47
47
|
# To add a new model: add an entry here with {"id": "model-id", "name": "Display Name"}
|
|
48
48
|
AVAILABLE_MODELS = [
|
|
49
|
-
{"id": "claude-opus-4-
|
|
50
|
-
{"id": "claude-sonnet-4-
|
|
49
|
+
{"id": "claude-opus-4-7", "name": "Claude Opus"},
|
|
50
|
+
{"id": "claude-sonnet-4-6", "name": "Claude Sonnet"},
|
|
51
51
|
]
|
|
52
52
|
|
|
53
53
|
# Map legacy model IDs to their current replacements.
|
|
54
54
|
# Used by get_all_settings() to auto-migrate stale values on first read after upgrade.
|
|
55
55
|
LEGACY_MODEL_MAP = {
|
|
56
|
-
"claude-opus-4-5-20251101": "claude-opus-4-
|
|
56
|
+
"claude-opus-4-5-20251101": "claude-opus-4-7",
|
|
57
|
+
"claude-opus-4-6": "claude-opus-4-7",
|
|
58
|
+
"claude-sonnet-4-5": "claude-sonnet-4-6",
|
|
59
|
+
"claude-sonnet-4-5-20250929": "claude-sonnet-4-6",
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
# List of valid model IDs (derived from AVAILABLE_MODELS)
|
|
@@ -65,7 +68,15 @@ VALID_MODELS = [m["id"] for m in AVAILABLE_MODELS]
|
|
|
65
68
|
_env_default_model = os.getenv("ANTHROPIC_DEFAULT_OPUS_MODEL")
|
|
66
69
|
if _env_default_model is not None:
|
|
67
70
|
_env_default_model = _env_default_model.strip()
|
|
68
|
-
|
|
71
|
+
# Auto-remap stale env-provided values (e.g. user's .env still pins 4.6)
|
|
72
|
+
if _env_default_model and _env_default_model in LEGACY_MODEL_MAP:
|
|
73
|
+
logging.getLogger(__name__).warning(
|
|
74
|
+
"ANTHROPIC_DEFAULT_OPUS_MODEL=%s is legacy; remapping to %s. "
|
|
75
|
+
"Update your .env to silence this warning.",
|
|
76
|
+
_env_default_model, LEGACY_MODEL_MAP[_env_default_model],
|
|
77
|
+
)
|
|
78
|
+
_env_default_model = LEGACY_MODEL_MAP[_env_default_model]
|
|
79
|
+
DEFAULT_MODEL = _env_default_model or "claude-opus-4-7"
|
|
69
80
|
|
|
70
81
|
# Ensure env-provided DEFAULT_MODEL is in VALID_MODELS for validation consistency
|
|
71
82
|
# (idempotent: only adds if missing, doesn't alter AVAILABLE_MODELS semantics)
|
|
@@ -119,6 +130,8 @@ class Project(Base):
|
|
|
119
130
|
path = Column(String, nullable=False) # POSIX format for cross-platform
|
|
120
131
|
created_at = Column(DateTime, nullable=False)
|
|
121
132
|
default_concurrency = Column(Integer, nullable=False, default=3)
|
|
133
|
+
auto_improve_enabled = Column(Boolean, nullable=False, default=False)
|
|
134
|
+
auto_improve_interval_minutes = Column(Integer, nullable=False, default=10)
|
|
122
135
|
|
|
123
136
|
|
|
124
137
|
class Settings(Base):
|
|
@@ -184,6 +197,7 @@ def _get_engine():
|
|
|
184
197
|
)
|
|
185
198
|
Base.metadata.create_all(bind=_engine)
|
|
186
199
|
_migrate_add_default_concurrency(_engine)
|
|
200
|
+
_migrate_add_auto_improve(_engine)
|
|
187
201
|
_SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=_engine)
|
|
188
202
|
logger.debug("Initialized registry database at: %s", db_path)
|
|
189
203
|
|
|
@@ -203,6 +217,25 @@ def _migrate_add_default_concurrency(engine) -> None:
|
|
|
203
217
|
logger.info("Migrated projects table: added default_concurrency column")
|
|
204
218
|
|
|
205
219
|
|
|
220
|
+
def _migrate_add_auto_improve(engine) -> None:
|
|
221
|
+
"""Add auto-improve columns if missing (for existing databases)."""
|
|
222
|
+
with engine.connect() as conn:
|
|
223
|
+
result = conn.execute(text("PRAGMA table_info(projects)"))
|
|
224
|
+
columns = [row[1] for row in result.fetchall()]
|
|
225
|
+
if "auto_improve_enabled" not in columns:
|
|
226
|
+
conn.execute(text(
|
|
227
|
+
"ALTER TABLE projects ADD COLUMN auto_improve_enabled INTEGER NOT NULL DEFAULT 0"
|
|
228
|
+
))
|
|
229
|
+
conn.commit()
|
|
230
|
+
logger.info("Migrated projects table: added auto_improve_enabled column")
|
|
231
|
+
if "auto_improve_interval_minutes" not in columns:
|
|
232
|
+
conn.execute(text(
|
|
233
|
+
"ALTER TABLE projects ADD COLUMN auto_improve_interval_minutes INTEGER NOT NULL DEFAULT 10"
|
|
234
|
+
))
|
|
235
|
+
conn.commit()
|
|
236
|
+
logger.info("Migrated projects table: added auto_improve_interval_minutes column")
|
|
237
|
+
|
|
238
|
+
|
|
206
239
|
@contextmanager
|
|
207
240
|
def _get_session():
|
|
208
241
|
"""
|
|
@@ -359,7 +392,11 @@ def list_registered_projects() -> dict[str, dict[str, Any]]:
|
|
|
359
392
|
p.name: {
|
|
360
393
|
"path": p.path,
|
|
361
394
|
"created_at": p.created_at.isoformat() if p.created_at else None,
|
|
362
|
-
"default_concurrency": getattr(p, 'default_concurrency', 3) or 3
|
|
395
|
+
"default_concurrency": getattr(p, 'default_concurrency', 3) or 3,
|
|
396
|
+
"auto_improve_enabled": bool(getattr(p, 'auto_improve_enabled', False)),
|
|
397
|
+
"auto_improve_interval_minutes": int(
|
|
398
|
+
getattr(p, 'auto_improve_interval_minutes', 10) or 10
|
|
399
|
+
),
|
|
363
400
|
}
|
|
364
401
|
for p in projects
|
|
365
402
|
}
|
|
@@ -386,7 +423,11 @@ def get_project_info(name: str) -> dict[str, Any] | None:
|
|
|
386
423
|
return {
|
|
387
424
|
"path": project.path,
|
|
388
425
|
"created_at": project.created_at.isoformat() if project.created_at else None,
|
|
389
|
-
"default_concurrency": getattr(project, 'default_concurrency', 3) or 3
|
|
426
|
+
"default_concurrency": getattr(project, 'default_concurrency', 3) or 3,
|
|
427
|
+
"auto_improve_enabled": bool(getattr(project, 'auto_improve_enabled', False)),
|
|
428
|
+
"auto_improve_interval_minutes": int(
|
|
429
|
+
getattr(project, 'auto_improve_interval_minutes', 10) or 10
|
|
430
|
+
),
|
|
390
431
|
}
|
|
391
432
|
finally:
|
|
392
433
|
session.close()
|
|
@@ -464,6 +505,71 @@ def set_project_concurrency(name: str, concurrency: int) -> bool:
|
|
|
464
505
|
return True
|
|
465
506
|
|
|
466
507
|
|
|
508
|
+
def get_project_auto_improve(name: str) -> tuple[bool, int]:
|
|
509
|
+
"""
|
|
510
|
+
Get a project's auto-improve configuration.
|
|
511
|
+
|
|
512
|
+
Args:
|
|
513
|
+
name: The project name.
|
|
514
|
+
|
|
515
|
+
Returns:
|
|
516
|
+
Tuple of (enabled, interval_minutes). Defaults to (False, 10) if
|
|
517
|
+
the project is not found or the columns are missing.
|
|
518
|
+
"""
|
|
519
|
+
_, SessionLocal = _get_engine()
|
|
520
|
+
session = SessionLocal()
|
|
521
|
+
try:
|
|
522
|
+
project = session.query(Project).filter(Project.name == name).first()
|
|
523
|
+
if project is None:
|
|
524
|
+
return (False, 10)
|
|
525
|
+
enabled = bool(getattr(project, "auto_improve_enabled", False))
|
|
526
|
+
interval = int(getattr(project, "auto_improve_interval_minutes", 10) or 10)
|
|
527
|
+
return (enabled, interval)
|
|
528
|
+
finally:
|
|
529
|
+
session.close()
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def set_project_auto_improve(
|
|
533
|
+
name: str,
|
|
534
|
+
enabled: bool | None = None,
|
|
535
|
+
interval_minutes: int | None = None,
|
|
536
|
+
) -> bool:
|
|
537
|
+
"""
|
|
538
|
+
Update a project's auto-improve configuration.
|
|
539
|
+
|
|
540
|
+
Either field can be updated independently by passing None for the other.
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
name: The project name.
|
|
544
|
+
enabled: If provided, set the enabled flag.
|
|
545
|
+
interval_minutes: If provided, set the interval in minutes (1-1440).
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
True if updated, False if the project wasn't found.
|
|
549
|
+
|
|
550
|
+
Raises:
|
|
551
|
+
ValueError: If interval_minutes is outside the 1-1440 range.
|
|
552
|
+
"""
|
|
553
|
+
if interval_minutes is not None and (interval_minutes < 1 or interval_minutes > 1440):
|
|
554
|
+
raise ValueError("interval_minutes must be between 1 and 1440")
|
|
555
|
+
|
|
556
|
+
with _get_session() as session:
|
|
557
|
+
project = session.query(Project).filter(Project.name == name).first()
|
|
558
|
+
if not project:
|
|
559
|
+
return False
|
|
560
|
+
|
|
561
|
+
if enabled is not None:
|
|
562
|
+
project.auto_improve_enabled = bool(enabled)
|
|
563
|
+
if interval_minutes is not None:
|
|
564
|
+
project.auto_improve_interval_minutes = int(interval_minutes)
|
|
565
|
+
|
|
566
|
+
logger.info(
|
|
567
|
+
"Set project '%s' auto_improve: enabled=%s, interval=%s",
|
|
568
|
+
name, enabled, interval_minutes,
|
|
569
|
+
)
|
|
570
|
+
return True
|
|
571
|
+
|
|
572
|
+
|
|
467
573
|
# =============================================================================
|
|
468
574
|
# Validation Functions
|
|
469
575
|
# =============================================================================
|
|
@@ -576,6 +682,28 @@ def get_setting(key: str, default: str | None = None) -> str | None:
|
|
|
576
682
|
return default
|
|
577
683
|
|
|
578
684
|
|
|
685
|
+
# Valid Claude Code reasoning/effort levels. Must match the CLI's --effort
|
|
686
|
+
# choices (low, medium, high, xhigh, max) — note: the SDK's Literal type at
|
|
687
|
+
# 0.1.61 omits "xhigh", but the string is forwarded to the CLI as-is and
|
|
688
|
+
# accepted there.
|
|
689
|
+
EffortLevel = Literal["low", "medium", "high", "xhigh", "max"]
|
|
690
|
+
VALID_EFFORT_LEVELS: tuple[EffortLevel, ...] = ("low", "medium", "high", "xhigh", "max")
|
|
691
|
+
DEFAULT_EFFORT: EffortLevel = "xhigh"
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def get_effort_setting() -> EffortLevel:
|
|
695
|
+
"""
|
|
696
|
+
Read the global reasoning-effort setting, falling back to ``xhigh``.
|
|
697
|
+
|
|
698
|
+
Unknown/invalid stored values are treated as missing so a DB corruption or
|
|
699
|
+
schema drift can't force the CLI into an unsupported mode.
|
|
700
|
+
"""
|
|
701
|
+
value = get_setting("effort")
|
|
702
|
+
if value in VALID_EFFORT_LEVELS:
|
|
703
|
+
return cast(EffortLevel, value)
|
|
704
|
+
return DEFAULT_EFFORT
|
|
705
|
+
|
|
706
|
+
|
|
579
707
|
def set_setting(key: str, value: str) -> None:
|
|
580
708
|
"""
|
|
581
709
|
Set a setting value (creates or updates).
|
|
@@ -604,7 +732,7 @@ def get_all_settings() -> dict[str, str]:
|
|
|
604
732
|
"""
|
|
605
733
|
Get all settings as a dictionary.
|
|
606
734
|
|
|
607
|
-
Automatically migrates legacy model IDs (e.g. claude-opus-4-
|
|
735
|
+
Automatically migrates legacy model IDs (e.g. claude-opus-4-6 -> claude-opus-4-7)
|
|
608
736
|
on first read after upgrade. This is a one-time silent migration.
|
|
609
737
|
|
|
610
738
|
Returns:
|
|
@@ -652,10 +780,10 @@ API_PROVIDERS: dict[str, dict[str, Any]] = {
|
|
|
652
780
|
"base_url": None,
|
|
653
781
|
"requires_auth": False,
|
|
654
782
|
"models": [
|
|
655
|
-
{"id": "claude-opus-4-
|
|
656
|
-
{"id": "claude-sonnet-4-
|
|
783
|
+
{"id": "claude-opus-4-7", "name": "Claude Opus"},
|
|
784
|
+
{"id": "claude-sonnet-4-6", "name": "Claude Sonnet"},
|
|
657
785
|
],
|
|
658
|
-
"default_model": "claude-opus-4-
|
|
786
|
+
"default_model": "claude-opus-4-7",
|
|
659
787
|
},
|
|
660
788
|
"kimi": {
|
|
661
789
|
"name": "Kimi K2.5 (Moonshot)",
|
|
@@ -683,11 +811,11 @@ API_PROVIDERS: dict[str, dict[str, Any]] = {
|
|
|
683
811
|
"requires_auth": True,
|
|
684
812
|
"auth_env_var": "ANTHROPIC_API_KEY",
|
|
685
813
|
"models": [
|
|
686
|
-
{"id": "claude-opus-4-
|
|
687
|
-
{"id": "claude-sonnet-4-
|
|
814
|
+
{"id": "claude-opus-4-7", "name": "Claude Opus"},
|
|
815
|
+
{"id": "claude-sonnet-4-6", "name": "Claude Sonnet"},
|
|
688
816
|
{"id": "claude-haiku-4-5", "name": "Claude Haiku"},
|
|
689
817
|
],
|
|
690
|
-
"default_model": "claude-opus-4-
|
|
818
|
+
"default_model": "claude-opus-4-7",
|
|
691
819
|
},
|
|
692
820
|
"ollama": {
|
|
693
821
|
"name": "Ollama (Local)",
|