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.
@@ -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
- if not is_initializer and iteration == 1:
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
- if agent_type == "initializer":
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)
@@ -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.agent_type:
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-6) pass through unchanged.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autoforge-ai",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "Autonomous coding agent with web UI - build complete apps with AI",
5
5
  "license": "AGPL-3.0",
6
6
  "bin": {
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-6", "name": "Claude Opus"},
50
- {"id": "claude-sonnet-4-5-20250929", "name": "Claude Sonnet"},
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-6",
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
- DEFAULT_MODEL = _env_default_model or "claude-opus-4-6"
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-5-20251101 -> claude-opus-4-6)
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-6", "name": "Claude Opus"},
656
- {"id": "claude-sonnet-4-5-20250929", "name": "Claude Sonnet"},
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-6",
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-6", "name": "Claude Opus"},
687
- {"id": "claude-sonnet-4-5", "name": "Claude Sonnet"},
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-6",
818
+ "default_model": "claude-opus-4-7",
691
819
  },
692
820
  "ollama": {
693
821
  "name": "Ollama (Local)",