Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ Trials can run on any provider:
export OPENAI_API_KEY=<api-key> # codex CLI + any openai/* model
export GEMINI_API_KEY=<api-key> # gemini CLI + any gemini/* model
export ZAI_API_KEY=<api-key> # GLM models via the mini-SWE-agent harness

# Optional: one-click hosted trial + public experiment link from the dashboard
export ODDISH_API_KEY=<full-scope-key>
```

## Usage
Expand All @@ -84,6 +87,7 @@ calibration but are sound are exported to `out/easy/`.

Re-running the same command resumes the run from wherever it parked. Ctrl-C stops active model
trials, keeps completed trials, and leaves unfinished work ready to resume.
The local dashboard starts automatically and opens the task page as soon as the run is created.

<details>
<summary>Options</summary>
Expand All @@ -98,7 +102,9 @@ trials, keeps completed trials, and leaves unfinished work ready to resume.
- `--config FILE.json` / `--preset NAME` — Full RunConfig (agents + per-model bands)
- `--brief TEXT` — Steer the task generation scope (eg "port the FFT subsystem...")
- `--review` — Pause at the two human gates (scope pick, final QA) instead of auto
- `--draft` — Export after Static CI with no sweeps or calibration
- `--yes` — Skip the cost preview confirmation
- `--no-open-dashboard` — Do not open the task page in a browser
- `--runs-dir PATH` — Choose the directory for runs (default: `.programsmith/runs`)
- `--allow-copyleft` — Allow copyleft-licensed sources

Expand Down Expand Up @@ -133,9 +139,12 @@ programsmith serve
programsmith stop
```

Serves the local dashboard at `http://localhost:8765`: live pipeline DAG for each run, agent
output, sweep results, file explorer, and optional review gates. Evaluation sweeps remain parked
unless `serve --spend` is used.
Serves the local dashboard at `http://localhost:8765`. Each exported task has a direct download
button and can launch one low-priority Oddish trial, show its agent trajectory, and return a public
experiment link. Build diagnostics, files, sweep results, and optional review gates remain
available without dominating the task page. Add a full-scope Oddish API key in Settings to enable
the hosted run; Oddish free-plan limits apply. Evaluation sweeps remain parked unless
`serve --spend` is used.
`programsmith serve` returns after the dashboard is healthy; `programsmith stop` is the explicit
shutdown command.

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dependencies = [
"rich>=13",
"fastapi>=0.110",
"uvicorn>=0.29",
"httpx>=0.27",
]

[project.urls]
Expand Down
19 changes: 18 additions & 1 deletion src/programsmith/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,17 @@ def _ensure_dashboard(runs_dir: Path, host: str, port: int, *, run_key: str | No
return f"{base}/run/{run_key}" if run_key else base


def _open_dashboard(url: str) -> bool:
"""Open a local dashboard without ever making task creation depend on desktop integration."""
if os.getenv("CI") or os.getenv("PROGRAMSMITH_NO_BROWSER"):
return False
try:
import webbrowser
return bool(webbrowser.open(url, new=2))
except Exception: # noqa: BLE001 — missing browser/headless sessions are expected
return False


def _cmd_purge_synthetic(args: argparse.Namespace) -> int:
"""Strip ALL synthetic data from a run and roll the FSM back to its last REAL stage. Removes any
manifest oracle/sweep entry tagged `simulated`, deletes a synthetic-built task skeleton when the
Expand Down Expand Up @@ -1118,6 +1129,9 @@ def _cmd_create_hero(args: argparse.Namespace) -> int:
ux.console.print(" check the owner/name spelling (private repos need a reachable URL); "
"pin a commit with --sha to skip HEAD resolution")
return 1
dashboard_run_url = f"{dashboard_url}/run/{key}" if dashboard_url else None
if dashboard_run_url and getattr(args, "open_dashboard", True):
_open_dashboard(dashboard_run_url)
if verdict == "exists":
ux.console.print(f"[dim]▶ {key}: run exists — resuming from where it parked[/dim]")
# A resumed run that already DROPPED can't be driven — re-surface WHY (the recorded gate
Expand All @@ -1134,7 +1148,6 @@ def _cmd_create_hero(args: argparse.Namespace) -> int:
if out.advice:
ux.console.print(f" {out.advice}")
return 1
dashboard_run_url = f"{dashboard_url}/run/{key}" if dashboard_url else None
outcome = ux.drive_run_foreground(run_dir, ctx=_drive_ctx(args), interval=args.interval,
notes_path=runs_dir.parent / "WORKFLOW_NOTES.md")
ux.summary_panel({key: outcome})
Expand Down Expand Up @@ -1614,6 +1627,10 @@ def _add_drive_flags(dp):
help="port for the auto-started dashboard (default: 8765)")
dp.add_argument("--dashboard-host", default="127.0.0.1",
help="host for the auto-started dashboard (default: 127.0.0.1)")
dp.add_argument("--open-dashboard", dest="open_dashboard", action="store_true", default=True,
help="open the task dashboard in your browser once the run is created (default: on)")
dp.add_argument("--no-open-dashboard", dest="open_dashboard", action="store_false",
help="start the dashboard without opening a browser tab")

# ---- hero commands (the README surface) ----
pcre = sub.add_parser(
Expand Down
16 changes: 16 additions & 0 deletions src/programsmith/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ class LhConfig(BaseModel):
openai_api_key: str | None = None
gemini_api_key: str | None = None # Google AI Studio key (GEMINI_API_KEY / GOOGLE_API_KEY)
zai_api_key: str | None = None # Z.ai key (GLM models)

# ---- hosted Oddish handoff -------------------------------------------------
# ProgramSmith can upload an exported task, launch one hosted trial, and publish the resulting
# experiment. The key belongs to the local operator's Oddish account and is stored with the same
# owner-only permissions as model credentials. It is never exposed unmasked by the dashboard.
oddish_api_key: str | None = None
oddish_api_url: str = "https://abundant-ai--api.modal.run"
oddish_dashboard_url: str = "https://www.oddish.app"
oddish_agent: str = "claude-code"
oddish_model: str = "anthropic/claude-sonnet-4-6"
# Per-TRIAL cost cap handed to the mini-swe solver (`-l`; 0 = disabled). Deliberately 0 by
# default: the product policy is cost preview + confirm before a sweep, not a silent cap that
# kills long legitimate trials. Always passed explicitly (mini's own default would cap quietly).
Expand Down Expand Up @@ -123,6 +133,11 @@ def load(cls) -> "LhConfig":
cfg.gemini_api_key = (os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
or cfg.gemini_api_key)
cfg.zai_api_key = os.getenv("ZAI_API_KEY") or cfg.zai_api_key
cfg.oddish_api_key = os.getenv("ODDISH_API_KEY") or cfg.oddish_api_key
cfg.oddish_api_url = os.getenv("ODDISH_API_URL") or cfg.oddish_api_url
cfg.oddish_dashboard_url = (
os.getenv("ODDISH_DASHBOARD_URL") or cfg.oddish_dashboard_url
)
if v := os.getenv("PROGRAMSMITH_TRIAL_COST_LIMIT"):
try:
cfg.trial_cost_limit = float(v)
Expand Down Expand Up @@ -153,6 +168,7 @@ def mask(v: str | None) -> str | None:
d["openai_api_key"] = mask(self.openai_api_key)
d["gemini_api_key"] = mask(self.gemini_api_key)
d["zai_api_key"] = mask(self.zai_api_key)
d["oddish_api_key"] = mask(self.oddish_api_key)
return d


Expand Down
Loading
Loading