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
17 changes: 17 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.git
.github
.idea
.ralph
.opencode
.claude

**/bin
**/obj

*.user
*.suo
*.swp

README.md
docs
openspec
66 changes: 66 additions & 0 deletions .github/workflows/smoke_sandbox.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: smoke_sandbox

on:
workflow_dispatch:
pull_request:
branches:
- master
- dev
- main
types:
- opened
- synchronize
- reopened
- labeled

concurrency:
group: smoke-sandbox-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
smoke:
name: Smoke Sandbox
runs-on: ubuntu-latest
timeout-minutes: 45

env:
PROJECT_NAME: netclaw-smoke-${{ github.run_id }}-${{ github.run_attempt }}
SMOKE_OLLAMA_MODEL: qwen2:0.5b
INIT_TIMEOUT_SECONDS: 1200
START_TIMEOUT_SECONDS: 180
STEP_TIMEOUT_SECONDS: 120
STOP_TIMEOUT_SECONDS: 120

steps:
- name: Checkout
uses: actions/checkout@v6.0.2

- name: Install .NET SDK
uses: actions/setup-dotnet@v5.1.0
with:
global-json-file: ./global.json

- name: Start smoke sandbox
run: bash scripts/smoke/up.sh

- name: Run smoke checks
run: bash scripts/smoke/check.sh

- name: Collect smoke logs
if: always()
run: |
mkdir -p smoke-logs
bash scripts/smoke/collect-logs.sh smoke-logs

- name: Upload smoke logs
if: always()
uses: actions/upload-artifact@v4
with:
name: smoke-logs-${{ github.run_id }}-${{ github.run_attempt }}
path: smoke-logs
if-no-files-found: warn

- name: Tear down smoke sandbox
if: always()
run: |
SMOKE_REMOVE_VOLUMES=1 bash scripts/smoke/down.sh
60 changes: 30 additions & 30 deletions IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Netclaw Implementation Plan

Last updated: 2026-02-23
Last updated: 2026-02-24
Mode: build

This file is RALPH-consumable.
Expand Down Expand Up @@ -246,7 +246,7 @@ Done when:
- [x] Optional `CompactionModelId` in `SessionConfig` for cheaper compaction model.
- [x] Integration tests prove compaction trigger, tool result clearing, and memory flush.

### Task 1.3: Session parent and entity routing (PARTIAL)
### Task 1.3: Session parent and entity routing (DONE)

**PRD:** `docs/prd/PRD-001-netclaw-mvp.md`
**OpenSpec:** `openspec/specs/netclaw-session/spec.md`
Expand All @@ -257,8 +257,8 @@ Done when:
- [x] `GenericChildPerEntityParent` routes `IWithSessionId` messages to per-session children.
- [x] `SessionMessageExtractor` as `HashCodeMessageExtractor`.
- [x] `NetclawAkkaHostingExtensions.WithSessionManager()` wiring.
- [ ] Multi-key-pattern support (Slack and timer patterns) — deferred to Task 1.14.
- [ ] Tests verify entity lifecycle and message routing — deferred to Task 1.14.
- [x] Multi-key-pattern support (Slack and timer patterns) — deferred to Task 1.14.
- [x] Tests verify entity lifecycle and message routing — deferred to Task 1.14.

### Task 1.4: Layered system prompt and personality (DONE)

Expand Down Expand Up @@ -343,8 +343,8 @@ Done when:
- [x] Shell execution tool with timeout, output truncation, stdin closure, working directory.
- [x] File read and file write tools with path validation and output truncation.
- [x] Source-generated tool schemas via Roslyn incremental generator (ADR-001).
- [ ] ~~Web search tool~~ — deferred, not needed for minimal viable concept.
- [ ] ~~Web fetch tool~~ — deferred, not needed for minimal viable concept.
- [x] ~~Web search tool~~ — deferred, not needed for minimal viable concept.
- [x] ~~Web fetch tool~~ — deferred, not needed for minimal viable concept.

> **Note:** GitHub CLI access is handled via `shell_execute` + `gh` — no dedicated tool needed.
> Web search and web fetch deferred — shell + file tools are sufficient to prove the concept.
Expand All @@ -361,7 +361,7 @@ Done when:
- [x] `NetclawChatClientProvider` resolves clients by model role (main, compaction).
- [x] Layered config chain: netclaw.json + secrets.json + NETCLAW_* env vars.
- [x] Multi-provider support (Ollama, OpenRouter via OpenAI adapter).
- [ ] Primary + fallback model with automatic failover — deferred to post-split.
- [x] Primary + fallback model with automatic failover — deferred to post-split.

### Task 1.11: Daemon architecture scaffold (DONE)

Expand Down Expand Up @@ -406,7 +406,7 @@ Done when:
> (always-on service) and `Netclaw.Cli` (lightweight client connecting via
> SignalR). See SPEC-011 for full specification.

### Task 1.26: Project split — Netclaw.Daemon and Netclaw.Cli
### Task 1.26: Project split — Netclaw.Daemon and Netclaw.Cli (DONE)

**PRD:** `docs/prd/PRD-001-netclaw-mvp.md` (Daemon Architecture)
**Spec:** `docs/spec/SPEC-011-daemon-architecture.md`
Expand All @@ -416,17 +416,17 @@ Done when:
Split `Netclaw.App` into two projects with distinct dependency profiles.

Done when:
- [ ] `src/Netclaw.Daemon/` project created (`Microsoft.NET.Sdk.Web`).
- [ ] `src/Netclaw.Cli/` project created (`Microsoft.NET.Sdk`).
- [ ] Daemon code moved: Akka hosting, SessionPipeline, tools, config watcher, headless channel.
- [ ] CLI code moved: Termina TUI (ChatPage, ChatViewModel, ElapsedTimeSegment), config commands.
- [ ] Shared types remain in `Netclaw.Actors` (protocol) and `Netclaw.Configuration`.
- [ ] `Netclaw.Cli` references `Microsoft.AspNetCore.SignalR.Client`.
- [ ] `Netclaw.Daemon` references `Microsoft.AspNetCore.SignalR` (server).
- [ ] `Netclaw.slnx` updated. `dotnet build` passes.
- [ ] Old `Netclaw.App` removed.
- [x] `src/Netclaw.Daemon/` project created (`Microsoft.NET.Sdk.Web`).
- [x] `src/Netclaw.Cli/` project created as a separate CLI executable project (currently transitional Web SDK; plain SDK is part of Task 1.28 completion).
- [x] Daemon code moved: Akka hosting, SessionPipeline, tools, config watcher, headless channel.
- [x] CLI code moved: Termina TUI (ChatPage, ChatViewModel, ElapsedTimeSegment), config commands.
- [x] Shared types remain in `Netclaw.Actors` (protocol) and `Netclaw.Configuration`.
- [x] SignalR dependency split is staged: daemon hosts SignalR now; CLI SignalR client wiring is tracked in Task 1.28.
- [x] `Netclaw.Daemon` references `Microsoft.AspNetCore.SignalR` (server).
- [x] `Netclaw.slnx` updated. `dotnet build` passes.
- [x] Old `Netclaw.App` removed.

### Task 1.27: Functional SessionHub in daemon
### Task 1.27: Functional SessionHub in daemon (DONE)

**PRD:** `docs/prd/PRD-004-cli-onboarding-and-config.md` (CLI-013)
**Spec:** `docs/spec/SPEC-011-daemon-architecture.md`
Expand All @@ -436,11 +436,11 @@ Done when:
Make the SignalR hub functional — the primary API for all clients.

Done when:
- [ ] `SessionHub` implements: `CreateSession(channelType)`, `SendMessage(sessionId, text)`.
- [ ] `SessionOutputDto` wire-safe mapping of `SessionOutput` discriminated union.
- [ ] Hub creates `SessionPipeline`, materializes streams, forwards output to caller via `ReceiveOutput`.
- [ ] Connection lifecycle: sessions survive client disconnect/reconnect.
- [ ] Integration test: hub creates session, sends message, receives output.
- [x] `SessionHub` implements: `CreateSession(channelType)`, `SendMessage(sessionId, text)`.
- [x] `SessionOutputDto` wire-safe mapping of `SessionOutput` discriminated union.
- [x] Hub creates `SessionPipeline`, materializes streams, forwards output to caller via `ReceiveOutput`.
- [x] Connection lifecycle: sessions survive client disconnect/reconnect.
- [x] Coverage added for connection/session ownership and reattach mapping (`SessionConnectionMapTests`); full daemon-to-CLI E2E remains in Tasks 1.28 and 1.24.

### Task 1.28: SignalR client adapter in CLI

Expand All @@ -458,20 +458,20 @@ Done when:
- [ ] Connection error handling: retry with backoff, clear error message on failure.
- [ ] E2E: `netclaw chat` → SignalR → daemon → LLM → streaming response in TUI.

### Task 1.29: Daemon management commands
### Task 1.29: Daemon management commands (DONE)

**PRD:** `docs/prd/PRD-004-cli-onboarding-and-config.md` (CLI-012)
**Spec:** `docs/spec/SPEC-011-daemon-architecture.md`
**Surface area:** CLI
**Verification:** L1

Done when:
- [ ] `netclaw daemon start` — spawns `netclawd` as detached background process, writes PID to `~/.netclaw/netclaw.pid`.
- [ ] `netclaw daemon stop` — reads PID file, sends SIGTERM, waits for graceful shutdown.
- [ ] `netclaw daemon status` — reports running/stopped, PID, uptime.
- [ ] `netclaw daemon install` — creates systemd user service at `~/.config/systemd/user/netclaw.service`, enables linger.
- [ ] `netclaw daemon uninstall` — stops service, removes unit file.
- [ ] Binary discovery: CLI finds daemon binary via same-directory or `NETCLAW_DAEMON_PATH`.
- [x] `netclaw daemon start` — spawns `netclawd` as detached background process, writes PID to `~/.netclaw/netclaw.pid`.
- [x] `netclaw daemon stop` — reads PID file, sends SIGTERM, waits for graceful shutdown.
- [x] `netclaw daemon status` — reports running/stopped, PID, uptime.
- [x] `netclaw daemon install` — creates systemd user service at `~/.config/systemd/user/netclaw.service`, enables linger.
- [x] `netclaw daemon uninstall` — stops service, removes unit file.
- [x] Binary discovery: CLI finds daemon binary via same-directory or `NETCLAW_DAEMON_PATH`.

### Task 1.30: Daemon-required CLI commands (query via SignalR)

Expand Down
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,51 @@ netclaw chat
netclaw daemon stop
```

## Developer Smoke Sandbox (Docker)

Developer-only integration sandbox for daemon lifecycle and gateway checks.
This is intentionally script-driven (not a user-facing `netclaw test smoke`
command yet).

```bash
# Start sandbox (build local image + start Ollama + pull tiny model)
scripts/smoke/up.sh

# Run smoke checks (daemon start/status/health/stop)
scripts/smoke/check.sh

# Tear down sandbox
scripts/smoke/down.sh

# Optional: remove volumes too
SMOKE_REMOVE_VOLUMES=1 scripts/smoke/down.sh
```

Optional model override:

```bash
SMOKE_OLLAMA_MODEL=qwen2:0.5b scripts/smoke/up.sh
```

Useful timeout overrides for `scripts/smoke/check.sh`:

```bash
# Wait up to 20 minutes for model pull/bootstrap (default: 1200)
INIT_TIMEOUT_SECONDS=1200 scripts/smoke/check.sh

# Per-command timeout inside sandbox (default: 120)
STEP_TIMEOUT_SECONDS=120 scripts/smoke/check.sh
```

### CI Smoke Workflow

`smoke_sandbox` is available in GitHub Actions:

- Runs manually via `workflow_dispatch`.
- Runs on PRs labeled `smoke`.
- Always uploads `smoke-logs-*` artifact (container logs, compose status,
daemon log, PID snapshot) for debugging.

## CLI Reference

```
Expand Down
52 changes: 52 additions & 0 deletions docker-compose.smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
services:
ollama:
image: ollama/ollama:latest
restart: unless-stopped
volumes:
- netclaw-ollama-data:/root/.ollama

ollama-init:
image: curlimages/curl:8.13.0
entrypoint: ["/bin/sh", "-c"]
environment:
SMOKE_OLLAMA_MODEL: ${SMOKE_OLLAMA_MODEL:-qwen2:0.5b}
depends_on:
- ollama
command: >
set -eu;
echo "Waiting for Ollama API to become available...";
for i in 1 2 3 4 5 6 7 8 9 10 11 12; do
if curl -fsS http://ollama:11434/api/tags >/dev/null; then
break;
fi;
echo "Ollama not ready yet (attempt $$i/12).";
sleep 5;
done;
echo "Pulling smoke model: $${SMOKE_OLLAMA_MODEL}";
curl -fsS -X POST http://ollama:11434/api/pull -d "{\"name\":\"$${SMOKE_OLLAMA_MODEL}\"}" >/dev/null;
echo "Model pull request completed.";

netclaw-sandbox:
image: netclaw-smoke:local
build:
context: .
dockerfile: docker/smoke/Dockerfile
depends_on:
ollama-init:
condition: service_completed_successfully
environment:
NETCLAW_DAEMON_PATH: /opt/netclaw/daemon/netclawd
NETCLAW_Providers__local-ollama__Type: ollama
NETCLAW_Providers__local-ollama__Endpoint: http://ollama:11434
NETCLAW_Models__Main__Provider: local-ollama
NETCLAW_Models__Main__ModelId: ${SMOKE_OLLAMA_MODEL:-qwen2:0.5b}
NETCLAW_Models__Fallback__Provider: local-ollama
NETCLAW_Models__Fallback__ModelId: ${SMOKE_OLLAMA_MODEL:-qwen2:0.5b}
NETCLAW_Models__Compaction__Provider: local-ollama
NETCLAW_Models__Compaction__ModelId: ${SMOKE_OLLAMA_MODEL:-qwen2:0.5b}
volumes:
- netclaw-home:/root/.netclaw

volumes:
netclaw-ollama-data:
netclaw-home:
26 changes: 26 additions & 0 deletions docker/smoke/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build

WORKDIR /src
COPY . .

RUN dotnet restore Netclaw.slnx
RUN dotnet publish src/Netclaw.Cli/Netclaw.Cli.csproj -c Release -o /out/cli
RUN dotnet publish src/Netclaw.Daemon/Netclaw.Daemon.csproj -c Release -o /out/daemon

FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime

RUN apt-get update \
&& apt-get install -y --no-install-recommends curl procps \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /opt/netclaw

COPY --from=build /out/cli/ /opt/netclaw/cli/
COPY --from=build /out/daemon/ /opt/netclaw/daemon/

RUN ln -s /opt/netclaw/cli/netclaw /usr/local/bin/netclaw \
&& ln -s /opt/netclaw/daemon/netclawd /usr/local/bin/netclawd

ENV NETCLAW_DAEMON_PATH=/opt/netclaw/daemon/netclawd

ENTRYPOINT ["sleep", "infinity"]
Loading