Skip to content

feat: set up a project on add (scaffold + analyze with Claude), observably - #278

Merged
centdix merged 9 commits into
mainfrom
webmux-project-setup
Jun 22, 2026
Merged

feat: set up a project on add (scaffold + analyze with Claude), observably#278
centdix merged 9 commits into
mainfrom
webmux-project-setup

Conversation

@hugocasa

@hugocasa hugocasa commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Adding a repo that has no .webmux.yaml now sets it up automatically instead of registering an unconfigured project, and the setup is observable in both the dashboard and the CLI:

  1. Creating .webmux.yaml — scaffold the starter config (the same template webmux init writes).
  2. Analyzing project structure — run the agent (Claude, or Codex if that's what's installed) headlessly to fill in services/panes/ports by inspecting the repo (best-effort).
  3. Project ready — register + persist, then open it.

Repos that already have a .webmux.yaml (or are already served) register immediately, unchanged.

How it works

  • ProjectInitTracker + runProjectInit (backend/src/services/project-init-service.ts): a hub-level tracker keyed by repo path drives creating_config → analyzing → ready (or failed). Analysis is best-effort — skipped if no agent is on PATH, non-fatal on error — so the starter config always ships. Terminal states expire via TTL so the map can't grow unbounded.
  • apiAddProject: repos with config register immediately and return { initializing: false, project }; repos without kick off the async setup job and return { initializing: true, path }. New GET /api/projects/init reports progress; the client polls it.
  • Reused, lifted helpers: the scaffold + repo-detection + headless-agent-run helpers that webmux init uses moved from bin/src/init-helpers.ts to backend/src/services/init-authoring.ts (with the generic run/which/getGitRoot/detectProjectName primitives in backend/src/lib/shell.ts, re-exported from bin/src/shared.ts), so the server and the CLI share one implementation. runInitAgentCommand gained an optional timeout so a hung agent can't stall setup.
  • Frontend: setUpProject() POSTs then polls projectInits, reporting each phase; EmptyProjects and the project switcher show the steps and navigate to the project once ready.
  • CLI: webmux project add on an unconfigured repo prints the same phases (via a DI-seamed, tested awaitProjectSetup), then the added line.

Observability choice

Phase-level progress over the existing poll pattern (matching worktree creation) rather than a live token stream — the project registers only once setup finishes, which avoids rebuilding a live project's runtime/sockets mid-flight.

Security / trust model

Setup spawns a headless agent (claude … --permission-mode bypassPermissions / codex … --sandbox workspace-write) with the added repo as cwd, triggered by POST /api/projects. The dashboard already grants arbitrary shell via terminal panes to anyone who can reach it, so it assumes a trusted/localhost network — this on-add agent run stays within that existing model and does not add a new exposure class. (Bun.serve binds all interfaces; deployments are expected to be localhost or a trusted LAN / behind SSH, as today.)

Also in this branch: broadened migration nudge

Tangential to setup-on-add but committed here. The "other webmux servers detected → run webmux project migrate" hint (from #271, which consolidated the old one-server-per-project model into a single dashboard) was project-only; it now fires on every command that reaches a project (oneshot/linear/project + worktree commands), except project migrate itself. It's a cheap local registry read, silent unless other servers exist, transient until you consolidate, on stderr, and amber (TTY only — plain when piped) to read as a nudge, not an error. The point is to push anyone still on the old per-project-server setup to update + consolidate.

Changes

  • api-contract: AddProjectResponse, ProjectInitPhase/ProjectInitState schemas, projectInits endpoint; addProject now returns AddProjectResponse.
  • backend: project-init-service.ts (new) + wiring in server.ts; init-authoring.ts lift + run timeout; lib/shell.ts (new).
  • frontend: setUpProject + projectInitPhaseLabel; EmptyProjects/ProjectSwitcher progress UI.
  • cli: webmux project add phase output; broadened + amber migration nudge in the webmux.ts dispatch.

Review polish (applied)

  • Agent-neutral phase label/copy (setup may run Codex, so it no longer claims "Claude").
  • Scaffolded defaultAgent falls back to claude (not codex) when neither agent is installed.
  • Dropped a redundant creating_config tracker write in apiAddProject.

Test plan

  • bun run test — backend 479, contract 4, bin 176, frontend 140; tsc --noEmit (backend) + svelte-check clean; bun run build ok.
  • Add a repo with a .webmux.yaml from the dashboard → registers immediately, opens.
  • Add a repo without a .webmux.yaml → watch "Creating .webmux.yaml" → "Analyzing project structure" → opens; .webmux.yaml is written and fleshed out.
  • webmux project add <repo-without-config> prints the same phases.
  • With claude/codex not on PATH → setup skips analysis and still registers with the starter config.
  • With another webmux server running, webmux project ls (and oneshot/linear/worktree commands) print the amber consolidation nudge on stderr; piping it shows plain text; project migrate does not print it.

Note: the agent-analysis path makes a real headless agent run on the repo; it's the one part not covered by automated tests (which inject the agent run).


Generated with Claude Code

hugocasa and others added 2 commits June 22, 2026 13:29
The scaffold + repo-detection + headless-Claude-run helpers that `webmux init`
uses (buildStarterTemplate, detectInitProjectContext, buildInitPromptSpec,
buildInitAgentCommand, runInitAgentCommand, stream parsing) move from
bin/src/init-helpers.ts to backend/src/services/init-authoring.ts so the
server can drive the same flow for an upcoming "set up project on add"
feature. `bin/src/init.ts` now imports them from the backend.

Generic process/git/repo primitives (run, which, getGitRoot, detectProjectName,
RunResult) move to backend/src/lib/shell.ts and are re-exported from
bin/src/shared.ts, so every existing `./shared` consumer is unchanged and the
backend can share them without a backend→bin import. Test moves alongside the
code into backend/src/__tests__.

No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…vably

Adding a repo that has no .webmux.yaml now runs setup automatically instead
of registering an unconfigured project: scaffold a starter .webmux.yaml →
analyze the repo with Claude to fill it in → register. Each phase is surfaced
in the dashboard and the CLI ("Creating .webmux.yaml" → "Analyzing project
structure with Claude" → ready).

Backend:
- ProjectInitTracker + runProjectInit (services/project-init-service.ts):
  hub-level, keyed by repo path; phases creating_config → analyzing → ready,
  or failed. Analysis is best-effort (skipped if no agent on PATH, non-fatal
  on error) so the starter config always ships. Terminal states TTL-expire.
- apiAddProject: repos with config (or already served) register immediately;
  repos without kick off the async job and return { initializing: true }.
  New GET /api/projects/init reports progress. The setup uses the lifted
  init-authoring helpers with a timeout so a hung agent can't stall it.

Contract: AddProjectResponse ({ initializing, path, project }), ProjectInit
phase/state schemas, and the projectInits endpoint.

Frontend: setUpProject() POSTs then polls projectInits, reporting phases;
EmptyProjects + ProjectSwitcher show the steps and open the project when ready.

CLI: `webmux project add` on an unconfigured repo prints the same phases via a
DI-seamed awaitProjectSetup(), then the added line. Parity with the UI.

Tests cover the tracker, orchestration (skip/best-effort/failure), and the CLI
poller; init-authoring gains a run timeout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jun 22, 2026

Copy link
Copy Markdown

Claude finished @hugocasa's task in 2m 50s —— View job


Code Review

Reviewed the full diff against origin/main. This is a clean, well-factored PR — the ProjectInitTracker/runProjectInit split is genuinely testable, the check-and-set in apiAddProject is race-free (no await between getByPath/hasProjectConfig/isActive/set, so concurrent POSTs for the same repo can't double-launch), best-effort analysis is handled correctly (skip when no agent, swallow on error, always register), and the poll pattern mirrors worktree creation. Ordering is right too: register() runs manager.add + reloadRoutes() before tracker.set("ready", …), so routes exist by the time a client navigates to /${prefix}/.

A few things worth addressing, mostly minor:

1. Phase label says "with Claude" but the agent may be Codex

authoringAgent() (server.ts:2366) falls back to codex when claude isn't on PATH, but the user-facing label is hardcoded to Claude in three places:

  • frontend/src/lib/utils.ts:147"Analyzing project structure with Claude"
  • bin/src/project-commands.ts:14 → same string
  • frontend/src/lib/EmptyProjects.svelte:46-47 → "analyzing the project with Claude" copy

On a Codex-only machine the UI/CLI will claim Claude is doing the analysis. Either make the copy agent-neutral ("Analyzing project structure…") or thread the chosen agent through. Low severity but user-visible. Fix this →

2. defaultAgent: codex scaffolded when no agent is installed

authoringAgent() returns which("claude") ? "claude" : "codex" — so with neither agent on PATH, scaffold still writes a starter config with workspace.defaultAgent: codex (and auto_name.provider: codex). For a "best-effort, no-agent" machine, claude is probably the more sensible default to write. Cosmetic, but the scaffolded file persists.

3. Trust boundary — adding a repo auto-runs a headless agent with elevated perms

projectInitDeps.analyze spawns claude … --permission-mode bypassPermissions / codex … --sandbox workspace-write with the added repo as cwd, triggered by an unauthenticated POST /api/projects. For a localhost-only dev tool this matches the existing trust model, but now that the dashboard serves multiple projects on one port, it's worth confirming the server binds to loopback only — otherwise "add this path" becomes a remote code-execution vector. Worth a sentence in the PR description acknowledging it; no code change needed if the bind is already localhost.

4. Minor: redundant creating_config set

apiAddProject (server.ts:2424) calls projectInitTracker.set(root, { phase: "creating_config" }) and then runProjectInit sets it again at project-init-service.ts:92. Harmless but you can drop the one in apiAddProject and rely on runProjectInitisActive after kickoff will still be true because runProjectInit's first statement is synchronous up to the first await.

5. Coverage gap (acknowledged)

The apiAddProject branching (existing / has-config / no-config) and apiListProjectInits aren't covered by an integration test — the new tests exercise runProjectInit and the CLI awaitProjectSetup poller in isolation, which is the right call, but the server wiring that picks which branch to take is untested. Not blocking.

Nits

  • backend/src/lib/shell.ts:14 keeps result.stdout as Buffer — pre-existing code moved verbatim, and it's a validated Bun.spawnSync boundary, so fine; just flagging it survived the move given backend/CLAUDE.md's no-as rule.
  • The cross-package bin → backend/src import in shared.ts is consistent with existing imports (oneshot.ts, init.ts, completions.ts) and bundles fine via the bun build bin/src/webmux.ts step, so no concern.

Nothing here is blocking; the architecture and error handling are solid. Items 1–2 are quick polish, item 3 is worth an explicit note.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 22, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
webmux 0d04707 Commit Preview URL

Branch Preview URL
Jun 22 2026, 02:43 PM

hugocasa and others added 5 commits June 22, 2026 13:45
Mirrors the CLI awaitProjectSetup coverage on the frontend surface: the
immediate (already-a-project) path, the initializing → poll → ready path
(resolves with the prefix, reports phases), and the failed path (rejects
with the server error). Stubs global fetch since the ts-rest client uses it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Agent-neutral phase label: setup may run Codex (authoringAgent falls back
  to it), so "Analyzing project structure with Claude" was wrong on a
  Codex-only machine. Drop the agent name from the label + EmptyProjects copy.
- Default the scaffolded config to Claude when neither agent is installed
  (was Codex): mirror init's `which("codex") && !which("claude")` rule, so a
  no-agent machine writes `defaultAgent: claude`.
- Drop the redundant `creating_config` set in apiAddProject — runProjectInit
  sets it synchronously before its first await, so the concurrent-launch
  guard still holds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Previously the "other webmux servers detected" nudge only fired in
`webmux project`. Broaden it to the other occasional, server-talking
commands (oneshot, linear) by hoisting the check into the webmux.ts
dispatch — the registry read is cheap and the warning is silent unless
peers actually exist.

Deliberately excludes the high-frequency worktree commands (list/open/
send/…), where a per-invocation warning would be noisy, and `project
migrate` itself (which does the consolidating). Centralizing the call
also removes the now-redundant one in project-commands.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the previous commit, which narrowed the warning to exclude
worktree commands. That call was driven by a non-hermetic e2e test (it read
the dev machine's real ~/.webmux registry), not by merit — so revert the
narrowing: the nudge now fires on every command that reaches a project
(oneshot/linear/project + worktree commands), still excluding `project
migrate`. It's silent unless other servers exist, transient until you
consolidate, and on stderr.

Make the webmux.test.ts e2e spawns hermetic by isolating HOME, so the
registry read sees an empty dir regardless of what's running locally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
It's a "you could consolidate" hint, not an error, so color it amber to
match the dashboard banner. ANSI only when stderr is a TTY, so piped or
redirected output stays plain. Stays on stderr (standard for diagnostics,
keeps stdout parseable).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hugocasa
hugocasa marked this pull request as ready for review June 22, 2026 14:19
@claude

claude Bot commented Jun 22, 2026

Copy link
Copy Markdown

Claude finished @hugocasa's task in 2m 27s —— View job


Code Review — PR #278

Reviewed the full diff against origin/main. This is a clean, well-factored PR. The ProjectInitTracker / runProjectInit split is genuinely unit-testable, best-effort analysis is handled correctly (skip when no agent, swallow on error, always register), terminal-state TTL eviction keeps the map bounded, and ordering is right — register() runs manager.add + reloadRoutes() before tracker.set("ready", …), so routes exist by the time a client navigates.

I also confirmed the three items from the earlier review were applied: agent-neutral phase labels (utils.ts:147, project-commands.ts, EmptyProjects.svelte copy), buildStarterTemplate defaulting defaultAgent to claude (init-authoring.ts:556), the dropped redundant creating_config write, and the trust-model note in the description. 👍

A few new things, all low severity — nothing blocking.

1. Concurrent add of the same fresh repo can register with the un-analyzed config

The isActive guard in apiAddProject only protects the no-config branch. But runProjectInit writes .webmux.yaml during scaffold before it finishes analyzing/registering. A second POST /api/projects for the same path arriving in that window (two tabs, or a double-click) now sees hasProjectConfig(root) === true and takes the immediate branch:

if (hasProjectConfig(root)) {
  const project = manager.add(root);   // registers from the STARTER config…
  reloadRoutes();                       // …while runProjectInit is still analyzing
  return jsonResponse({ initializing: false,});
}

manager.add is idempotent (register() returns the existing project), so there's no double-registration or crash. But the runtime gets built from the starter config, and the analyzed config written moments later won't be reflected until a reload — and that second client navigates straight in, bypassing the phase UI. Functionally fine (starter config is valid), just not the enriched result. Cheap guard: also bail to the polling path when projectInitTracker.isActive(root) is true, before the hasProjectConfig check. Fix this →

2. A transient poll failure aborts the client flow while setup keeps running

In both setUpProject (api.ts) and awaitProjectSetup (project-commands.ts), an exception from hubApi.projectInits() / deps.poll() propagates out of the loop and surfaces as a setup failure — even though the backend job continues and may well succeed. A single hiccup during a multi-minute analysis poll then shows the user "failed" for a project that's actually fine. Wrapping the poll call in a try/catch that just continues to the next tick (until the deadline) would make this resilient. Low severity.

Nits

  • bin/src/project-commands.ts ends with a stray extra blank line (the diff adds a trailing newline at EOF).
  • backend/src/lib/shell.ts:14 keeps result.stdout as Buffer — pre-existing code moved verbatim from shared.ts, validated Bun.spawnSync boundary, so fine; just flagging it survived the move given backend/CLAUDE.md's no-as rule.
  • frontend/src/lib/api.ts delay() uses setTimeout — correct for the frontend (browser), just noting it's not the Bun-API path the CLI uses (Bun.sleep).

Coverage

The new tests exercise runProjectInit and the CLI awaitProjectSetup poller in isolation — the right call. The apiAddProject three-way branch (existing / has-config / no-config) and apiListProjectInits server wiring remain untested, and the concurrent-add window in item 1 lives exactly in that untested branching. Not blocking, but a small integration test there would be worth it.

Overall: solid architecture and error handling. Items 1–2 are quick hardening for the concurrent / flaky-network edges; the rest is cosmetic.
· webmux-project-setup

hugocasa and others added 2 commits June 22, 2026 16:25
From the PR review:

- apiAddProject: check projectInitTracker.isActive(root) BEFORE
  hasProjectConfig. Setup writes .webmux.yaml mid-run, so a second
  concurrent POST for the same repo would otherwise see that half-written
  config and register from it before analysis finished — bypassing the
  enriched result and the phase UI. Now it joins the in-flight setup's
  polling path instead.
- setUpProject (frontend) + awaitProjectSetup (CLI): swallow a transient
  poll failure and retry until the deadline, instead of surfacing "failed"
  for a setup that's still running fine on the server.
- Trim a trailing blank line in project-commands.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The only server-side log the feature added was an error-level line when the
best-effort analysis fails — but that path recovers (keeps the starter
config and still registers), so it's a warn, not an error. Also the happy
path and the terminal-failure path logged nothing, leaving a stuck
"analyzing" or a real setup failure invisible in server logs.

Now runProjectInit logs: "setting up <root>" at start, "<root> ready as
<prefix>" on success, warn when analysis is skipped/fails (recoverable),
and error on a genuine (terminal) setup failure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@centdix
centdix merged commit b6411da into main Jun 22, 2026
2 checks passed
@centdix
centdix deleted the webmux-project-setup branch June 22, 2026 15:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants