Skip to content
Open
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
447 changes: 447 additions & 0 deletions .design/linked-groves-ui.md

Large diffs are not rendered by default.

633 changes: 633 additions & 0 deletions .design/workstation-onboarding-wizard.md

Large diffs are not rendered by default.

343 changes: 343 additions & 0 deletions .design/workstation-onboarding.md

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions .tasks/ci-full-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Run ci-full and Fix All Issues

**Branch:** workstation-improvements
**Goal:** Run `make ci-full` and fix every failure, including pre-existing ones unrelated to our changes.

---

## Steps

1. Run `make ci-full` and capture all failures
2. Fix each failure category in turn:
- `fmt-check` failures → run `make fmt` then re-check
- `web` (Vite build) failures → fix TypeScript/JS build errors
- `web-typecheck` failures → fix TypeScript type errors
- `lint` failures → fix lint errors (go vet, staticcheck, etc.)
- `golangci-lint` failures → fix golangci-lint findings
- `test-fast` failures → fix failing tests (including pre-existing ones)
- `build` failures → fix compile errors

3. After fixing each category, re-run that specific step to confirm it passes before moving on
4. Run full `make ci-full` at the end to confirm everything passes together

## Known pre-existing test failures (from earlier run)

These tests in `pkg/config` were failing before our changes — fix them too:
- `TestIsInsideProject`
- `TestRequireProjectPath_NoProjectError`
- `TestFindProjectRoot_HubContextNoScion_Disabled`
- `TestDiscoverProjects_GitProjectWithExternalConfigUsesWorkspaceMarkerProjectID`
- `TestIsHubContext`

Investigate each failure message and fix the root cause.

## Commit instructions

- Commit fixes in logical groups (e.g. one commit for fmt fixes, one for test fixes, etc.)
- Use clear commit messages describing what was broken and what was fixed
- Run `make ci-full` one final time to confirm all green before the last commit
- Do not open PRs — commit directly to `workstation-improvements`
111 changes: 111 additions & 0 deletions .tasks/onboarding-bugs-3-4-5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Fix: Onboarding Bugs 3, 4, 5

**Branch:** workstation-improvements
**Commit all changes to the current branch.**

---

## Bug 3 — Wizard skips to step 4 on fresh install

**File:** `web/src/components/pages/onboarding.ts`

**Problem:** `initialize()` auto-advances `currentStep` based on backend status flags (`runtimeOK`, `harnessesSeeded`). After `scion init --machine`, both are already true, so the wizard jumps to step 4 (Images) on first launch, skipping Identity, System Check, Runtime, and Harness steps.

**Fix:** The resume logic must only fire if the user has previously progressed through the wizard. Add a `previouslyStarted` check — use `sessionStorage.getItem('onboardingStarted')` (set it when the user clicks "Next" for the first time). Only run the resume auto-advance if `previouslyStarted` is true:

```typescript
async initialize() {
const status = await this.fetchStatus();
const previouslyStarted = sessionStorage.getItem('onboardingStarted') === 'true';

if (previouslyStarted) {
// Resume logic: advance past already-complete steps
if (status.identitySet && this.currentStep === 0) this.currentStep = 1;
if (status.runtimeOK && this.currentStep <= 2) this.currentStep = Math.max(this.currentStep, 3);
if (status.harnessesSeeded && this.currentStep <= 3) this.currentStep = Math.max(this.currentStep, 4);
}
// Always start at step 0 on fresh install regardless of backend status
}
```

Set `sessionStorage.setItem('onboardingStarted', 'true')` when the user advances from step 0 for the first time (i.e. in the "Next" handler for step 0, or on any step advance).

Clear `sessionStorage.removeItem('onboardingStarted')` when the wizard completes (step "Done").

---

## Bug 4 — Image names missing registry prefix on step 5

**Files:** `web/src/components/pages/onboarding.ts` and `pkg/hub/system_handlers.go`

### Frontend fix

**Problem:** The event handler extracts only the harness name via `imageNameToHarness()` and reconstructs a partial name for display. The full registry-qualified image name is in the SSE event as `d['image']` but is discarded.

**Fix:** Store and display the full image name from the event data. In the image status map, use the full image name as the key (not the harness name), OR store both:

```typescript
// In the SSE event handler (around line 1004-1016):
if (d['image']) {
const fullImageName = d['image'] as string; // e.g. "ghcr.io/homebrew-scion/scion-claude:latest"
const status = d['status'] as string;
// Store by full image name
const next = new Map(this.imageStatuses);
next.set(fullImageName, { status, error: d['error'] as string | undefined });
this.imageStatuses = next;
}
```

In the render template (around line 872), display the full image name:
```typescript
// Show: "ghcr.io/homebrew-scion/scion-claude:latest" instead of "scion-claude:latest"
${[...this.imageStatuses.entries()].map(([image, info]) => html`
<div class="image-status">
<code>${image}</code>
<span class="status-${info.status}">${info.status}</span>
${info.error ? html`<span class="error">${info.error}</span>` : ''}
</div>
`)}
```

### Backend: add registry to /system/status

**Problem:** The frontend has no way to verify what registry is configured.

**Fix:** Add `ImageRegistry string` to `OnboardingStatus` in `pkg/hub/system_handlers.go` and populate it by reading `image_registry` from settings (via `config.LoadSettings("")` → `settings.ImageRegistry` or however it's stored). The frontend can display this in the images step header: "Pulling from: ghcr.io/homebrew-scion".

---

## Bug 5 — "Build locally" fails for Homebrew installs

**File:** `pkg/hub/system_handlers.go` (handleSystemImagesBuild, ~line 492-504)

**Problem:** Build script is not present in Homebrew installations. The handler falls back to a CWD/binary-relative path that doesn't exist.

**Fix:** Add an availability check before the build option is shown or invoked:

In `handleSystemImagesBuild`:
```go
// Check build script exists before attempting
buildScript := resolveBuildScript() // existing resolution logic
if buildScript == "" {
http.Error(w, `{"error":"local builds require a source checkout; use image pull instead","buildUnavailable":true}`, http.StatusUnprocessableEntity)
return
}
```

In `GET /system/status`, add `BuildAvailable bool` that returns `true` only if the build script can be resolved. Frontend uses this to:
- Hide the "Build locally" button when `!status.buildAvailable`
- Show an explanatory note: "Pre-built images are available from ghcr.io/homebrew-scion. Local builds require a source checkout."

This ensures that for Homebrew installs the user only sees the pull path, while developer checkouts still get the build option.

---

## Commit Instructions

- `fix: only resume wizard progress if user has previously started onboarding (bug 3)`
- `fix: display full registry-qualified image names in wizard step 5 (bug 4)`
- `fix: add registry and buildAvailable to system/status; hide build option for brew installs (bug 5)`
- Run `go build ./...` and `go vet ./...` before committing Go changes
- Do not open PRs — commit directly to `workstation-improvements`
55 changes: 55 additions & 0 deletions .tasks/phantom-daemon-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Fix: Phantom Daemon After Reinstall

**Branch:** workstation-improvements
**Key files:** `pkg/daemon/daemon.go`, `cmd/server_daemon.go`
**Commit all changes to the current branch.**

## Fix 1 — Port conflict detection in `scion server start`

In `cmd/server_daemon.go` `runServerStartOrDaemon()` (around line 34), after the existing `StatusComponent()` check, add a port probe:

```go
// Check for phantom processes holding server ports even without a PID file
if phantomPorts := detectOccupiedPorts(cfg); len(phantomPorts) > 0 {
fmt.Fprintf(os.Stderr, "Error: the following ports are already in use: %v\n", phantomPorts)
fmt.Fprintf(os.Stderr, "A previous server process may be running without a PID file.\n")
fmt.Fprintf(os.Stderr, "Run 'scion server stop --force' to kill any process on these ports.\n")
return fmt.Errorf("port conflict: ports %v are occupied", phantomPorts)
}
```

Implement `detectOccupiedPorts(cfg)` in `pkg/daemon/daemon.go` or a new `pkg/daemon/ports.go`:
- Try to bind (and immediately release) each server port (default: 8080 for web/hub, 9800 for broker, 9810 for broker gRPC or whatever ports the config uses)
- If binding fails → port is occupied
- Return the list of occupied ports

Look at how the server resolves its ports from `cfg` (search for `cfg.Hub.Port`, `cfg.RuntimeBroker.Port` etc.) to know which ports to probe.

Use `net.Listen("tcp", fmt.Sprintf(":%d", port))` — if it succeeds, close it immediately and mark as free; if it fails with EADDRINUSE, mark as occupied.

## Fix 2 — `scion server stop --force`

In `cmd/server_daemon.go` `runServerStop()` (around line 165), add a `--force` flag:

```
--force Kill any process listening on the server ports, even without a PID file
```

When `--force` is set:
1. Probe the server ports (reuse `detectOccupiedPorts`)
2. For each occupied port, find the PID of the process holding it using `lsof -ti :<port>` (macOS/Linux) or `ss -tlnp` (Linux fallback)
3. Kill the PID with SIGTERM, wait up to 3s, then SIGKILL if still running
4. Report what was killed

Simple implementation using `exec.Command("lsof", "-ti", fmt.Sprintf(":%d", port))` — parse the output as a PID, then `syscall.Kill(pid, syscall.SIGTERM)`.

If no PID file and no occupied ports, print "No running server found."

Add `--force` to the `stop` command's flags in `cmd/server_daemon.go`.

## Commit Instructions

- `feat: detect port conflicts on server start to catch phantom daemons`
- `feat: add scion server stop --force to kill phantom processes by port`
- Run `go build ./...` and `go vet ./...` before committing
- Do not open PRs — commit directly to `workstation-improvements`
66 changes: 66 additions & 0 deletions .tasks/phase-0-1-foundations-identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Phase 0+1: Foundations, Identity & Labels

**Branch:** workstation-improvements
**Design docs:** `.design/workstation-onboarding.md` §7, `.design/workstation-onboarding-wizard.md`, `.design/linked-groves-ui.md`
**Commit all changes to the current branch as you go.**

---

## Phase 0 — Foundations

### 0.1 — `Workstation bool` on `hub.ServerConfig`

File: `pkg/hub/web.go` (or wherever `ServerConfig` is defined — search for the struct)

- Add `Workstation bool` field to `hub.ServerConfig`
- In `cmd/server_foreground.go` around line 774, set `cfg.Workstation = !productionMode` when building the server config
- On the server struct, store it: `s.workstation bool`
- Add two helpers:
- `requireWorkstation(next http.Handler) http.Handler` — middleware that returns 404 if `!s.workstation`
- `assertLoopback(r *http.Request) error` — checks `r.RemoteAddr` is loopback (127.x or ::1)
- These will gate all `/system/*` and filesystem endpoints

### 0.2 — `GetEmbeddedBrokerID()` accessor

- Add a method on the server (or hub config) that returns the pre-generated embedded broker ID from `settings.yaml`
- This is used in W5 when the two-step linked-grove create needs to find the co-located broker

---

## Phase 1 — Cosmetic Identity (W2) + Developer Token Relabel (W3)

### 1.1 — Identity fields on DevAuthConfig / V1AuthConfig (W2)

Files:
- `pkg/config/hub_config.go` — `DevAuthConfig` struct (around line 142-158): add `Username`, `DisplayName`, `Email string` fields
- `pkg/config/settings_v1.go` — `V1AuthConfig` (around line 383-388): same additions
- `pkg/hub/devauth.go` — `DevUser` construction (around line 26-49):
- **Keep the stable UUID** (`be67fbc9-...`) unchanged
- Read `Username`/`DisplayName`/`Email` from config
- If unset, default to OS user via `os/user` (`user.Current()` → `u.Username`, `u.Name`)
- Email default: `<osusername>@localhost`

Add a small `PUT /api/v1/system/identity` endpoint:
- Body: `{ "displayName": "...", "email": "..." }`
- Writes to `DevAuthConfig` in `settings.yaml` via the config save path
- Returns the updated identity
- Protected by `requireWorkstation`

### 1.2 — "Developer token" relabel (W3)

This is a text/copy pass only — **no code logic changes**:
- `cmd/server_daemon.go` `printWorkstationQuickstart()` (line 361-384): change "dev token" → "developer token" in the printed output
- CLI help strings referencing "dev token" in `cmd/` — update to "developer token"
- Web copy in `web/src/` — grep for "dev token" / "dev-token" in UI strings and update display text (not variable names, not `scion_dev_` format, not env var names — those stay)
- Any user-facing docs in `docs-site/` or `docs-repo/`

---

## Commit Instructions

- Commit Phase 0 work as one commit: `feat: add Workstation flag and workstation-only middleware to ServerConfig`
- Commit Phase 1 work as two commits:
- `feat: make dev identity configurable, default to OS user (W2)`
- `chore: relabel dev token as "developer token" in UI and docs (W3)`
- Run `go build ./...` and `go vet ./...` before each commit to verify no compile errors
- Do not open PRs — commit directly to the `workstation-improvements` branch
89 changes: 89 additions & 0 deletions .tasks/phase-2-system-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Phase 2: System API Endpoints

**Branch:** workstation-improvements
**Design docs:** `.design/workstation-onboarding-wizard.md` §6, `.design/workstation-onboarding.md` §7
**Prereq:** Phase 0+1 complete (Workstation flag and requireWorkstation middleware exist)
**Commit all changes to the current branch as you go.**

---

## Overview

Add thin `GET/PUT /api/v1/system/*` API endpoints that wrap existing Go logic. All are gated by `requireWorkstation` (404 in production). All require normal auth.

---

## 2.1 — Refactor doctor into a returnable function + `GET /system/check`

- In `cmd/doctor.go` (line 30-261) or `pkg/runtime/doctor.go` (line 15-41), extract the core check logic into a function `GatherDiagnostics(ctx context.Context, cfg *config.Config) ([]DiagnosticResult, error)` that returns structured results instead of printing them.
- `DiagnosticResult` struct: `{ Name, Status ("pass"|"warn"|"fail"), Message string }`
- Add `GET /api/v1/system/check` handler:
- Calls `GatherDiagnostics`
- Returns JSON: `{ "results": [...], "ready": bool }` where `ready` = no "fail" results

## 2.2 — `GET` and `PUT /system/runtime`

- `GET /api/v1/system/runtime`:
- Calls `config.DetectLocalRuntime()` (`pkg/config/runtime_detect.go:52-75`)
- Returns: `{ "detected": "docker"|"podman"|"container", "configured": "...", "available": bool }`
- `PUT /api/v1/system/runtime`:
- Body: `{ "runtime": "docker"|"podman"|"container" }`
- Validates the choice, writes `active_profile` (or runtime setting) to `settings.yaml`
- Returns the updated runtime config

## 2.3 — `ComputeOnboardingStatus` + `GET /system/status`

Compute a struct describing the onboarding state of the machine:

```go
type OnboardingStatus struct {
Initialized bool // ~/.scion/settings.yaml exists
IdentitySet bool // DevAuthConfig has username set (non-default)
RuntimeOK bool // a runtime is detected and reachable
HarnessesSeeded bool // at least one harness-config exists
ImagesPresent bool // at least one harness image is present (optional check)
HasWorkspace bool // at least one project exists
Complete bool // all required steps done
}
```

- `GET /api/v1/system/status` returns this struct as JSON
- Used by the frontend to detect first-run and resume the wizard

Also wire up `PUT /api/v1/system/identity` here if not done in Phase 1:
- Body: `{ "displayName": "...", "email": "..." }`
- Writes to `DevAuthConfig`, returns updated identity

## 2.4 — `POST /system/init`

- Body: `{ "harnesses": ["claude", "gemini", "codex", "opencode"] }` (subset allowed)
- Calls `config.InitMachine()` (`pkg/config/init.go:548-620`) if not already initialized
- Seeds only the selected harness-configs (filter the full seed set)
- Returns `{ "ok": true, "initialized": true }`
- Idempotent: safe to call on an already-initialized machine (no-op or partial re-seed)

---

## Route Registration

Register all endpoints in the existing `MountHubAPI` function (`pkg/hub/web.go:518-527`), wrapped with `requireWorkstation`:

```go
// System / onboarding endpoints (workstation only)
r.With(s.requireWorkstation).Get("/system/status", s.handleSystemStatus)
r.With(s.requireWorkstation).Get("/system/check", s.handleSystemCheck)
r.With(s.requireWorkstation).Get("/system/runtime", s.handleGetRuntime)
r.With(s.requireWorkstation).Put("/system/runtime", s.handlePutRuntime)
r.With(s.requireWorkstation).Post("/system/init", s.handleSystemInit)
r.With(s.requireWorkstation).Put("/system/identity", s.handlePutIdentity)
```

Put handler implementations in a new file: `pkg/hub/system_handlers.go`

---

## Commit Instructions

- One commit per logical unit is fine, or bundle as: `feat: add /system/* API endpoints for workstation onboarding (W1/W2)`
- Run `go build ./...` and `go vet ./...` before committing
- Do not open PRs — commit directly to `workstation-improvements`
Loading