Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
bda74c0
feat(cli): continuum update splits Carl (pull) vs Dev (build) paths
joelteply Apr 17, 2026
1b02fc8
feat(skills): continuum:update skill + install.sh installer hook
joelteply Apr 17, 2026
b11874f
feat(skills): continuum:status + continuum:doctor + continuum:chat
joelteply Apr 17, 2026
e80485d
docs: surface continuum skills in README + SETUP.md
joelteply Apr 17, 2026
b832cdd
fix(setup): verify GPU backend engaged + enable TCP programmatically …
joelteply Apr 17, 2026
ec2ffac
feat(setup): post-start inference probe proves chat works end-to-end
joelteply Apr 17, 2026
207c728
feat(scripts): verify-personas.sh — merge-gate acceptance test
joelteply Apr 17, 2026
0a159dd
fix(scripts): stop swallowing errors — fail loud per Joel's hard rule
joelteply Apr 17, 2026
b96a652
fix(generator): result fields required by default — compile-time enfo…
joelteply Apr 17, 2026
57ad850
docs(setup): UID-mismatch on Linux — root-owned bind-mount files (gap…
joelteply Apr 17, 2026
ef8d182
fix(orchestrator): seed-on-boot retries IPC ready instead of 3s race …
joelteply Apr 17, 2026
3fb9b66
feat(doctor): stale-image detection — git rev label vs repo HEAD (gap…
joelteply Apr 17, 2026
8b4d3e7
fix(ipc): always schedule reconnect — boot-race no longer wedges clie…
joelteply Apr 17, 2026
941adf9
chore: remove debug-investigation console.logs from PR891 (Copilot re…
joelteply Apr 17, 2026
2372043
fix(concurrency): check sysctlbyname rc + per-OS RAM probes + cache +…
joelteply Apr 17, 2026
c945ada
fix: usize overflow in matmul FLOPs + stale priority comment (Copilot…
joelteply Apr 17, 2026
50d8105
fix(rag): query cache promise no longer permanently caches a rejectio…
joelteply Apr 17, 2026
2a07e63
fix(install): auto-generate per-install LiveKit API_KEY + API_SECRET
joelteply Apr 17, 2026
8003cb0
fix(doctor): config-keys count display — '0\\n0 keys' bug
joelteply Apr 17, 2026
6e5b463
fix(generator): add required? to CommandNaming.ResultSpec
joelteply Apr 17, 2026
18f5212
test(verify): scripts/verify-pr-913.sh — runtime PROOF, not just diff…
joelteply Apr 17, 2026
c3ec853
fix(verify-913): cross-platform timeout (gtimeout fallback for macOS)
joelteply Apr 17, 2026
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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,23 @@ cd continuum/src && npm install && npm start
Detailed dev environment + platform-specific gotchas: **[docs/SETUP.md](docs/SETUP.md)**.
</details>

<details>
<summary>Claude Code users — bonus skills</summary>

Continuum ships a set of [Claude Code](https://claude.com/claude-code) skills so your IDE's Claude can invoke continuum operations without leaving the editor. Opt-in: `install.sh` drops them into `~/.claude/skills/` only if Claude Code is detected — otherwise silent no-op.

| Skill | What it does |
|---|---|
| `/continuum:update` | Pull latest images, refresh forged Qwen (`--dev` flag for source rebuild) |
| `/continuum:status` | Show containers, personas, DMR backend, grid nodes |
| `/continuum:doctor` | Diagnose install + runtime problems, narrow to the root cause |
| `/continuum:chat @<persona> <msg>` | Send a message to a continuum persona from your IDE |

**Why this matters for devs**: the dev who's already coding in Claude Code gets continuum as a nearby `/command`, not a context switch. The long-term direction is continuum's own persona layer replaces the Claude-Code-as-IDE pattern entirely, but for the transition period this is how a dev using both systems gets them to talk to each other.

Continuum does NOT require Claude Code. Carl (end-user) uses the widget. Skills are purely additive for the dev audience.
</details>

| Client | Status |
|--------|--------|
| **Browser** | Working — [Positron](docs/positron/POSITRON-ARCHITECTURE.md) widget system (Lit + Shadow DOM) |
Expand Down
100 changes: 93 additions & 7 deletions bin/continuum
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
# continuum wake <node> Wake + restart a downed grid node
# continuum provision Pull config from a grid node
# continuum transfer <n> Deploy Continuum to a new machine
# continuum update Git pull + rebuild + restart
# continuum update Carl: git pull + docker compose pull + up (fast, default)
# Dev: add --dev flag for build-from-source
# continuum doctor Diagnose common problems
#
# Installed by: curl -fsSL continuum.homes/install | bash
Expand Down Expand Up @@ -490,13 +491,55 @@ cmd_update() {
exit 1
fi
cd "$COMPOSE_DIR"
echo -e "${BLUE}📥 Updating...${RESET}"
git pull origin main
echo -e "${BLUE}🔨 Rebuilding...${RESET}"
docker compose build --parallel
echo -e "${BLUE}🔄 Restarting...${RESET}"

# Default = Carl path: pull prebuilt images from ghcr (fast).
# --build / --dev = Dev path: rebuild from source (slow, needed when touching Rust/TS).
local mode="pull"
for arg in "$@"; do
case "$arg" in
--build|--dev) mode="build" ;;
--help|-h)
echo "continuum update — pull latest and restart."
echo ""
echo " continuum update Carl path: git pull + docker compose pull + up -d"
echo " + refresh Qwen model in DMR. Fast (~30s on warm cache)."
echo " continuum update --dev Dev path: git pull + docker compose build + up -d."
echo " Slower but picks up local source changes."
echo ""
return 0 ;;
esac
done

echo -e "${BLUE}📥 Fetching latest source...${RESET}"
git pull origin main || echo -e "${YELLOW}⚠️ git pull failed — continuing with local source.${RESET}"

if [ "$mode" = "pull" ]; then
echo -e "${BLUE}📦 Pulling latest images from ghcr...${RESET}"
if ! docker compose pull; then
echo -e "${RED}❌ Image pull failed. If this is a dev machine and you want to rebuild from source instead:${RESET}"
echo -e " continuum update --dev"
exit 1
fi

# Refresh the default forged Qwen in DMR so new quantization / eval releases
# land without requiring the user to know about docker model pull. Idempotent
# on the docker model CLI — no-op if DMR isn't installed / TCP toggle off.
if docker model --help &>/dev/null 2>&1; then
echo -e "${BLUE}🧠 Refreshing forged Qwen in Docker Model Runner...${RESET}"
docker model pull hf.co/continuum-ai/qwen3.5-4b-code-forged-GGUF 2>&1 | tail -3 || \
echo -e "${YELLOW}⚠️ Qwen refresh failed (continuing — you can retry manually: docker model pull hf.co/continuum-ai/qwen3.5-4b-code-forged-GGUF)${RESET}"
fi
else
echo -e "${BLUE}🔨 Rebuilding images from source (dev mode — slow)...${RESET}"
docker compose build --parallel
fi

echo -e "${BLUE}🔄 Restarting services...${RESET}"
docker compose up -d

echo -e "${GREEN}✅ Updated${RESET}"
echo -e " Check status: ${DIM}continuum status${RESET}"
echo -e " Diagnose: ${DIM}continuum doctor${RESET}"
}

cmd_tray_data() {
Expand Down Expand Up @@ -612,7 +655,13 @@ cmd_doctor() {

# Config
if [ -f "$CONTINUUM_HOME/config.env" ]; then
local count; count=$(grep -c "=" "$CONTINUUM_HOME/config.env" 2>/dev/null || echo 0)
# grep -c prints the count then exits 1 if there are 0 matches. The old
# `|| echo 0` then ran and appended "0" to the variable — output was
# "0\n0 keys" on any empty config. Capture grep's output, ignore exit code,
# default to 0 if empty.
local count
count=$(grep -c "=" "$CONTINUUM_HOME/config.env" 2>/dev/null || true)
count=${count:-0}
echo -e " ${GREEN}●${RESET} Config: $count keys in $CONTINUUM_HOME/config.env"
if grep -q "TS_AUTHKEY" "$CONTINUUM_HOME/config.env" 2>/dev/null; then
echo -e " ${GREEN}●${RESET} Grid auth key: configured"
Expand Down Expand Up @@ -730,6 +779,43 @@ cmd_doctor() {
fi
fi

# Stale-image detection — compare the running container's git revision
# (injected by docker/metadata-action via the org.opencontainers.image.revision
# label on every CI publish) to the local repo HEAD. Memento spent hours on
# PR891 chasing "why isn't my fix in the running binary" before realizing
# the container was a week-old image. This check turns that silent gap into
# a visible warning.
if find_compose 2>/dev/null; then
cd "$COMPOSE_DIR"
local core_name
core_name=$(docker compose ps --format '{{.Name}}' 2>/dev/null | grep -E 'continuum-core(-1)?$' | head -1 || true)
if [ -n "$core_name" ]; then
# Container's image revision label = git SHA the image was built from
local image_id; image_id=$(docker inspect "$core_name" --format '{{.Image}}' 2>/dev/null || echo "")
local image_revision=""
if [ -n "$image_id" ]; then
image_revision=$(docker inspect "$image_id" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' 2>/dev/null || echo "")
fi
# Local repo HEAD
local repo_head; repo_head=$(git -C "$COMPOSE_DIR" rev-parse HEAD 2>/dev/null || echo "")
if [ -n "$image_revision" ] && [ -n "$repo_head" ]; then
# Compare prefixes — image labels are full SHAs, git short-rev is 7 chars
local img_short="${image_revision:0:8}"
local repo_short="${repo_head:0:8}"
if [ "$img_short" = "$repo_short" ]; then
echo -e " ${GREEN}●${RESET} Image revision: $img_short (matches repo HEAD)"
else
echo -e " ${YELLOW}●${RESET} Image revision: $img_short (repo HEAD is $repo_short — image is stale)"
echo -e " The running container was built from a different commit than your local repo."
echo -e " Pull the latest published image: ${DIM}continuum update${RESET}"
echo -e " Or, if you want THIS commit's code: ${DIM}continuum update --dev${RESET}"
fi
elif [ -z "$image_revision" ]; then
echo -e " ${DIM}○${RESET} Image revision: no label (image built without docker/metadata-action; can't verify freshness)"
fi
fi
fi

echo ""
}

Expand Down
32 changes: 28 additions & 4 deletions docs/SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ While inference runs, you should see GPU utilization spike to 70%+ and memory gr
- **`docker model status` says `latest-cpu`:** the GPU toggle is off, or Docker Desktop hasn't finished installing the CUDA backend. Re-check Settings → AI, click Apply, wait 60 seconds.
- **Personas reply but `nvidia-smi` shows no activity:** the host-side TCP toggle is off. The container can't reach DMR; it's likely silently routing to a CPU path. Toggle it on.
- **Build fails with apt timeouts:** WSL networking issue, often resolved by `--network=host` or by `wsl --shutdown` to reset DNS. See [docs/infrastructure/WINDOWS-WSL2-INSTALL-GUIDE.md](infrastructure/WINDOWS-WSL2-INSTALL-GUIDE.md) for the full playbook.
- **`docker push` silently 401s from WSL2 even after `docker login` succeeded** *(dev-path only — Carl doesn't push):* Docker Desktop writes `credsStore: desktop.exe` into WSL2's `~/.docker/config.json`, which delegates auth to the Windows Credential Manager — but WSL2 can't invoke the Windows GUI credential manager, so pushes silently 401. Fix: pipe a PAT into `docker login` from inside WSL, which stores creds inline in `config.json` instead of delegating: `echo '<PAT>' \| docker login ghcr.io -u <user> --password-stdin`. Or `gh auth token \| docker login ghcr.io -u <user> --password-stdin` if the `gh` CLI is installed with `write:packages` scope.

---

Expand Down Expand Up @@ -204,6 +205,16 @@ Then open `http://localhost:9003`, send a chat. Same expected throughput as Wind

- **`runtime: nvidia` not recognized:** install [`nvidia-container-toolkit`](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) and restart the Docker daemon.
- **Container starts but no GPU access:** check `nvidia-smi` from inside the container with `docker exec continuum-continuum-core-1 nvidia-smi` — if blank, the runtime isn't binding.
- **Permission denied on `~/.continuum/sockets/*` from the host user:** Docker containers run as root by default, so files they create in the bind-mounted `~/.continuum/` directory end up root-owned and unreadable by your normal user account. Symptom: CLI commands like `./jtag ping` fail with `EACCES: permission denied` even though the services are healthy. Fix:
```bash
# Reclaim ownership (run as your normal user, not root)
sudo chown -R "$(id -u):$(id -g)" ~/.continuum
# Then set the container UID/GID to match yours so future writes stay yours
echo "PUID=$(id -u)" >> ~/.continuum/config.env
echo "PGID=$(id -g)" >> ~/.continuum/config.env
docker compose down && docker compose up -d
```
This is a known Linux-only friction (Mac and Windows don't hit it because Docker Desktop's VM handles the UID translation). Tracked for a code-side fix that runs the container as the host UID by default.

---

Expand All @@ -229,23 +240,36 @@ The tag flows through `docker-compose*.yml` for all 7 image variants. Use this t

## Skills + helpers

### Continuum skills for Claude Code (dev-only, opt-in)

If you use [Claude Code](https://claude.com/claude-code) as your IDE, `install.sh` drops a set of Continuum skills into `~/.claude/skills/` so you can invoke Continuum operations as `/commands` without leaving the editor. Silent no-op if you don't have Claude Code — Continuum's core functionality is entirely independent.

| Skill | What it does |
|---|---|
| `/continuum:update` | Pull latest images + refresh forged Qwen in DMR (`--dev` flag = rebuild from source) |
| `/continuum:status` | Containers + personas + DMR backend + grid nodes + widget URL |
| `/continuum:doctor` | Diagnose install/runtime problems, narrow to the root cause |
| `/continuum:chat @<persona> <msg>` | Send a message to a Continuum persona from the IDE; reply comes back through the chat log |

**Direction**: these skills are the bridge for devs currently in Claude Code. Continuum's own persona layer replaces the need for them over time — the steady state is "you just talk to personas in the widget." But while devs are on both systems, skills let the two talk cleanly.

### airc — bring your AI mesh

If you're running continuum and want your IDE's Claude (or your friend's Claude) to peer with continuum's personas over a shared mesh, install [airc](https://github.com/CambrianTech/airc):
If you want your IDE's Claude (or a coworker's Claude) to peer with continuum's personas over a shared mesh, install [airc](https://github.com/CambrianTech/airc):

```bash
curl -fsSL https://raw.githubusercontent.com/CambrianTech/airc/main/install.sh | bash
```

Then your Claude Code can use the `/connect` skill to join a continuum mesh — useful for live install troubleshooting where the AI on the other side has hands-on context.
Then `/airc:connect <join-string>` from any Claude Code session joins the mesh. Useful for live install troubleshooting where the AI on the other side has hands-on context.

### `continuum doctor` — post-install health check
### `continuum doctor` — post-install health check (CLI)

```bash
continuum doctor
```

Verifies submodules, IPC sockets, GPU vs CPU backend, scheduler vs llama-server, cloud key presence, disk free. Run after install or any time chat behavior gets weird.
Verifies submodules, IPC sockets, GPU vs CPU backend, scheduler vs llama-server, cloud key presence, disk free. Run after install or any time chat behavior gets weird. The `/continuum:doctor` skill wraps this and translates the output for the user — same check, IDE-accessible.

### Where the logs live

Expand Down
53 changes: 53 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,27 @@ ok "Source: $INSTALL_DIR"
# fallback (~/.local/bin) when sudo would prompt without a TTY.
mod_continuum_bin_link "$INSTALL_DIR/bin/continuum"

# ── 3c. Install Claude Code skills (opt-in, only if ~/.claude exists) ─
# Continuum ships a set of slash-command skills (continuum:update,
# eventually continuum:status, continuum:doctor, continuum:chat) that
# let an AI in any project invoke continuum operations directly —
# "plug continuum into your IDE Claude" pattern, mirrors airc's
# skills install.
#
# Opt-in: only installs when ~/.claude/skills/ exists (indicating the
# user has Claude Code installed and is running). Silent no-op otherwise
# — continuum's core functionality doesn't require Claude Code.
if [ -d "$HOME/.claude/skills" ] && [ -d "$INSTALL_DIR/skills" ]; then
info "Installing Continuum skills into ~/.claude/skills/ (Claude Code detected)..."
for skill_dir in "$INSTALL_DIR/skills"/*/; do
[ -d "$skill_dir" ] || continue
skill_name=$(basename "$skill_dir")
mkdir -p "$HOME/.claude/skills/$skill_name"
cp -r "$skill_dir"/* "$HOME/.claude/skills/$skill_name/"
ok " Installed skill: /$(basename "$skill_name" | tr '-' ':')"
done
fi

# ── 4. Configuration ───────────────────────────────────────
mkdir -p "$CONTINUUM_DATA"

Expand All @@ -426,6 +447,38 @@ else
ok "Config exists: $CONFIG_FILE"
fi

# ── 4b. LiveKit API credentials — auto-generate per-install ─
# LiveKit ships with `--dev` keys (API_KEY=devkey, API_SECRET=secret)
# baked into the LiveKit-server binary's dev mode. Fine for local Carl
# (LiveKit container only listens on localhost). NOT fine for any
# Tailscale-grid-exposed deployment — anyone on your tailnet could
# join your voice/video session with the dev keys.
#
# Generate strong random API_KEY + API_SECRET on first install. Idempotent:
# only generate if not already present in config.env. Per-install unique
# secrets without requiring the user to do anything. Memento's PR914
# voice migration uses these via getSecret().
if ! grep -q '^LIVEKIT_API_KEY=' "$CONFIG_FILE" 2>/dev/null; then
if command -v openssl &>/dev/null; then
LK_KEY=$(openssl rand -hex 16) # 32 chars — readable in logs
LK_SECRET=$(openssl rand -hex 32) # 64 chars — full strength
{
echo ""
echo "# LiveKit credentials — auto-generated at install for per-instance uniqueness"
echo "# (LiveKit's --dev mode defaults are insecure for any networked deployment)"
echo "LIVEKIT_API_KEY=$LK_KEY"
echo "LIVEKIT_API_SECRET=$LK_SECRET"
} >> "$CONFIG_FILE"
ok "LiveKit credentials: generated (LIVEKIT_API_KEY/SECRET in config.env)"
else
warn "openssl not found — skipping LiveKit credential generation. Install will use insecure dev defaults."
warn " Manually generate: openssl rand -hex 16 (key), openssl rand -hex 32 (secret)"
warn " Add LIVEKIT_API_KEY= and LIVEKIT_API_SECRET= to $CONFIG_FILE"
fi
else
ok "LiveKit credentials: already present in config.env"
fi

# ── 5. TLS certs (Tailscale) ──────────────────────────────
TS_HOSTNAME=""
if command -v tailscale &>/dev/null; then
Expand Down
40 changes: 40 additions & 0 deletions scripts/lib/repo-root.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/bin/bash
# repo-root.sh — shared helper. Source this, then $REPO_ROOT is set.
#
# Usage:
# source "$(dirname "${BASH_SOURCE[0]}")/lib/repo-root.sh"
# cd "$REPO_ROOT/src"
#
# Works from any CWD. Derives from the location of this file, then walks up
# to find the nearest parent directory containing `docker-compose.yml` + `src/`.
# Exports REPO_ROOT. Idempotent — safe to source multiple times.

# Already set by an outer script? Trust it if valid.
if [ -n "${REPO_ROOT:-}" ] && [ -f "$REPO_ROOT/docker-compose.yml" ] && [ -d "$REPO_ROOT/src" ]; then
return 0 2>/dev/null || true
fi

# Resolve this file's directory, following symlinks correctly.
_repo_root_self="${BASH_SOURCE[0]}"
while [ -L "$_repo_root_self" ]; do
_repo_root_dir="$(cd "$(dirname "$_repo_root_self")" && pwd)"
_repo_root_self="$(readlink "$_repo_root_self")"
case "$_repo_root_self" in /*) ;; *) _repo_root_self="$_repo_root_dir/$_repo_root_self" ;; esac
done
_repo_root_dir="$(cd "$(dirname "$_repo_root_self")" && pwd)"

# Walk up looking for the root marker (docker-compose.yml + src/ together).
_candidate="$_repo_root_dir"
while [ "$_candidate" != "/" ]; do
if [ -f "$_candidate/docker-compose.yml" ] && [ -d "$_candidate/src" ]; then
export REPO_ROOT="$_candidate"
unset _repo_root_self _repo_root_dir _candidate
return 0 2>/dev/null || true
fi
_candidate="$(dirname "$_candidate")"
done

# Walked to / and found nothing.
echo "❌ repo-root.sh: could not locate continuum repo root (no docker-compose.yml+src/ found walking up from $_repo_root_dir)" >&2
unset _repo_root_self _repo_root_dir _candidate
return 2 2>/dev/null || exit 2
Loading
Loading