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
14 changes: 9 additions & 5 deletions .github/scripts/build-macos-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,18 @@ tar -xzf "dist/${ARCHIVE}" -C "$smoke_root/unpacked"
package_dir="$smoke_root/unpacked/ferrus-${TARGET}"
bin_path="$package_dir/ferrus"
test -x "$bin_path"
test -x "$package_dir/ferrus-nano"
env HOME="$smoke_root/home" "$package_dir/ferrus-nano" --version
test -f "$package_dir/README.md"
test -f "$package_dir/LICENSE"
test -f "$package_dir/NOTICE"
file "$bin_path"
test "$(lipo -archs "$bin_path")" = arm64
minos="$(otool -l "$bin_path" | awk '$1 == "cmd" && $2 == "LC_BUILD_VERSION" { build_version = 1; next } build_version && $1 == "minos" { print $2; build_version = 0 }')"
echo "Packaged macOS deployment target: $minos"
test "$minos" = "$MACOSX_DEPLOYMENT_TARGET"
for executable in "$bin_path" "$package_dir/ferrus-nano"; do
file "$executable"
test "$(lipo -archs "$executable")" = arm64
minos="$(otool -l "$executable" | awk '$1 == "cmd" && $2 == "LC_BUILD_VERSION" { build_version = 1; next } build_version && $1 == "minos" { print $2; build_version = 0 }')"
echo "Packaged macOS deployment target: $minos"
test "$minos" = "$MACOSX_DEPLOYMENT_TARGET"
done

env HOME="$smoke_root/home" "$bin_path" --version
(
Expand Down
1 change: 1 addition & 0 deletions .github/scripts/package-unix-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mkdir -p dist
staging="dist/ferrus-${TARGET}"
mkdir -p "${staging}"
cp "target/${TARGET}/dist/ferrus" "${staging}/ferrus"
cp "target/${TARGET}/dist/ferrus-nano" "${staging}/ferrus-nano"
cp README.md LICENSE NOTICE "${staging}/"
tar -C dist -czf "dist/${ARCHIVE}" "ferrus-${TARGET}"
(
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ jobs:
$staging = Join-Path "dist" "ferrus-$env:TARGET"
New-Item -ItemType Directory -Force -Path $staging | Out-Null
Copy-Item "target/$env:TARGET/dist/ferrus.exe" "$staging/ferrus.exe"
Copy-Item "target/$env:TARGET/dist/ferrus-nano.exe" "$staging/ferrus-nano.exe"
Copy-Item README.md, LICENSE, NOTICE $staging
Compress-Archive -Path $staging -DestinationPath "dist/$env:ARCHIVE" -Force
$hash = (Get-FileHash "dist/$env:ARCHIVE" -Algorithm SHA256).Hash.ToLowerInvariant()
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ jobs:
- name: Test Nano unit suite
run: "cargo test --locked --features nano-openai,nano-mcp --bin ferrus nano::"

- name: Test Nano headless integration
run: cargo test --locked --features nano-openai,nano-mcp --test nano_headless
- name: Test Nano headless and standalone integration
run: cargo test --locked --features nano-openai,nano-mcp --test nano_headless --test nano_standalone

macos-release:
runs-on: macos-latest
Expand Down
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ src/
agent_id.rs # stable agent IDs and MCP server names
legacy_state.rs # legacy STATE.json import shape
agents/ # agent launcher and config adapters
nano/ # native Executor runtime, headless protocol, and HQ conversations
nano/ # shared native runtime, managed/HQ adapters, standalone host and binary
platform/ # OS-specific process and lifecycle helpers
state/ # scoped human-readable artifact helpers
checks/ # configured check runner
Expand Down Expand Up @@ -294,7 +294,9 @@ implement both role adapters, model normalization, headless prompt transport whe
version and config behavior, registration wiring, and focused tests. `opencode` is
experimental: it binds a project to one working directory by Git root commit, so it is
currently reliable only for the Supervisor and Reviewer roles.
Nano is Executor-only; keep its native runtime and adapter under `src/nano/`.
Nano managed roles are Executor-only; keep its runtime and adapters under `src/nano/`.
`ferrus-nano` is a separate trusted-local headless host: no registration, task lease, or
`ferrus.db`. Share core tools/contracts while keeping standalone completion separate from submit.

Claude Code role-scoped MCP configuration is stored in `.claude/mcp-supervisor.json` and
`.claude/mcp-executor.json`; permissions are stored in `.claude/settings.local.json`.
Expand Down
8 changes: 7 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
name = "ferrus"
version = "0.5.1-alpha.1"
edition = "2024"
default-run = "ferrus"
rust-version = "1.95.0"
license = "Apache-2.0"
repository = "https://github.com/ferrus-dev/ferrus"
Expand All @@ -23,6 +24,11 @@ exclude = [
name = "ferrus"
path = "src/main.rs"

[[bin]]
name = "ferrus-nano"
path = "src/nano/main.rs"
required-features = ["nano-openai"]

[features]
default = ["update-check"]
update-check = ["dep:semver", "dep:ureq"]
Expand All @@ -32,7 +38,7 @@ nano-mcp = ["neva/client", "dep:jsonschema"]
[dependencies]
neva = { version = "0.6.2", features = ["server", "di", "legacy-spec"] }
jsonschema = { version = "0.56.0", optional = true }
tokio = { version = "1.52.3", features = ["fs", "io-util", "macros", "process", "rt-multi-thread", "sync", "time"] }
tokio = { version = "1.52.3", features = ["fs", "io-util", "macros", "process", "rt-multi-thread", "signal", "sync", "time"] }
clap = { version = "4.6.1", features = ["derive"] }
serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.151"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ non-Git projects run in the project directory with one Executor. Checks run befo
work resumes with review feedback. `/status`, `/tasks`, `/runs`, and `/events`
provide local inspection; `ferrus doctor` and `ferrus recover` handle consistency and interrupted work.

`ferrus-nano` runs headlessly in an unregistered Git or non-Git directory; see [standalone Nano](docs/ferrus-nano-standalone.md).
With Nano, `/executor` opens a direct workspace conversation without a queued task;
`/attach NAME` opens a running or historical managed conversation. See [interactive Nano](docs/ferrus-nano-interactive.md).

Expand Down
169 changes: 169 additions & 0 deletions docs/ferrus-nano-standalone.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# Standalone Nano

`ferrus-nano` runs a headless coding session in an explicitly selected directory.
Git and Ferrus registration are optional. It does not start HQ, load a selected spec,
claim a task, or open/create `ferrus.db`. Interactive standalone UI is tracked in #88.

## Installation and provider settings

Release archives include `ferrus-nano` alongside `ferrus`; the installers copy both
when present. For a source installation:

```sh
cargo install ferrus --locked --profile dist --features nano-openai,nano-mcp --bin ferrus-nano
```

For a checkout:

```sh
cargo build --release --features nano-openai,nano-mcp --bin ferrus-nano
```

`nano-openai` enables the executable. Add `nano-mcp` only when external peers are
needed. Default `cargo build` and `cargo install ferrus` retain their existing feature
set and do not include the standalone executable.

Use the same private provider settings as managed Nano. Resolution order is
`--config`, `FERRUS_NANO_CONFIG`, then `~/.ferrus/nano.toml` (the user's home directory
on Windows too). An explicit path must be absolute. For local LM Studio, a minimal
file contains:

```toml
base_url = "http://127.0.0.1:1234/v1"
model = "your-loaded-model"
reasoning_effort = "none"
```

Create it with owner-only permissions: `chmod 600 ~/.ferrus/nano.toml` on Unix, or a
protected DACL granting only its owner on Windows. See [provider settings](ferrus-nano-provider.md)
for credentials, optional generation parameters, transport limits, and session budgets.
Standalone never provisions project metadata or uses provider secrets in command children.

## Running a request

```sh
ferrus-nano --workspace /absolute/path/to/repository --prompt "Fix the parser and run its tests"
printf '%s\n' "Inspect the parser" | ferrus-nano --workspace /absolute/path/to/repository
```

The workspace must already exist. Requests are bounded UTF-8 input. Stdout contains
the final model response; stderr contains the session ID, journal path, and diagnostics. Exit zero
requires a durable `ModelFinished` journal record. It means the model completed its
response, not that Ferrus checks passed or a managed submission was approved.
Ctrl-C cancels the session and stops/joins owned command processes before recording
the terminal outcome where possible. Unexpected process death still requires recovery.

Native tools include `read_file`, `search_text`, `apply_patch`, `exec`, `read_process`,
`read_output`, `stop_process`, `load_instructions`, and `repository_fallback`.
Root `AGENTS.md` is loaded automatically; nested guidance and explicit skills use the
same bounded instruction loader as managed Nano. The system policy is standalone:
there is no `wait_for_task`, `check`, `submit`, consultation, or task-backed human wait.
Validation commands run through `exec`; they do not issue managed check receipts.

### Trusted-local execution

Launching the executable opts into trusted-local workspace and shell execution.
File tools use the shared path, digest, quota, and protected-metadata rules. Commands
run with the user's OS permissions, without an OS sandbox or per-command approval UI;
they can affect files outside the workspace. Their environment is allowlisted and
does not inherit provider credentials, Git overrides, startup hooks, or Ferrus authority.
External MCP peers receive only their explicitly configured environment.

Standalone has no managed Git ownership. Its policy leaves staging, commits, reset,
and worktree management to the user unless explicitly requested. Do not run it against
a workspace concurrently edited by HQ, another harness, or another user. Standalone
sessions themselves share a machine-local advisory workspace lock, including when
different journal storage paths are selected; this is not an orchestration task lease.

## Journals and continuation

Default storage is `~/.ferrus/standalone/<workspace-id>/`. The ID comes from filesystem
directory identity, so case/normalization aliases share the same local workspace slot.
`--storage /absolute/private/directory` selects another storage directory whose parent
must exist; it must be outside the workspace. Storage is owner-only and bound to that
workspace. It contains no registry, task rows, or orchestration database.

The existing append-only journal, checkpoint, output, and command-spool contracts apply:

```text
<storage>/workspace.json
<storage>/workspace.lock
<storage>/nano/sessions/<session-id>/events.jsonl
<storage>/nano/sessions/<session-id>/commands/
```

The separate workspace lock lives under `~/.ferrus/standalone/locks/`. Use
`--session-id NAME` for a known ID; otherwise Nano generates one and prints it to stderr.

```sh
ferrus-nano --workspace /absolute/path/to/repository --resume PREVIOUS_ID \
--prompt "Continue with the next change"
```

Continuation creates a new journal. It imports a bounded previous transcript as
untrusted historical user content, not as replayed tool calls or live provider state.
It retains cumulative token, model-turn, tool-call, and retry accounting; the explicit
new request resets no-progress tracking and gets a fresh per-attempt elapsed deadline.
An interrupted provider reservation is conservatively charged. Current settings and
scoped guidance are loaded again. Oversized history fails closed rather than being
silently dropped.

Recovery repairs only an incomplete journal tail and seals an interrupted prior attempt.
It never re-executes recorded operations or trusts old PIDs. Pending/unknown effects,
unconfirmed commands, and incomplete/inconsistent output spools reject continuation
before inference. Reconcile them manually before starting fresh work. Confirmed exited,
cancelled, and timed-out commands require complete spools. Completed source journals
remain unchanged.

## Optional local context

Repository indexing is explicit and independent of Ferrus project configuration:

```sh
ferrus-nano --workspace /absolute/path/to/repository --index-graph
ferrus-nano --workspace /absolute/path/to/repository --graph --prompt "Find the parser entry points"
```

`--index-graph` builds/refreshes `<storage>/repo-graph.db` and exits without loading
provider settings. It uses the shipped local discovery/extractor policy and a dedicated
standalone repository namespace/publication. `--graph` exposes the three native
repository retrieval tools; queries never build indexes or resolve managed task views.
The same request/result/time/byte caps apply. Each request pins the selected snapshot;
requested repository snippets pass the existing hash-verified content boundary.

Retrieval does not rescan the full repository to claim freshness. External edits leave
freshness `unknown`; current file tools remain the source of workspace evidence. Nano
records known mutations before effects and blocks stale graph retrieval until explicit
reindex. Graph-disabled/missing/stale coverage uses labeled workspace fallback. Working
sets never reuse graph evidence as current without compatible verified revisions.

An existing curated project-memory sidecar can be supplied read-only:

```sh
ferrus-nano --workspace /absolute/path/to/repository \
--memory-sidecar /absolute/path/to/project-memory.db \
--memory-namespace local:ferrus --memory-project PROJECT_ID \
--prompt "Inspect prior specification decisions"
```

The explicit namespace/project identify the sidecar's portable scope. Memory queries
pin its published revision, report freshness independently, and cross into repository
facts only through exact revision/snapshot link sets. Standalone does not discover,
index, or author memory sources. Memory source snippets have no standalone verified
content adapter yet; use structural memory context with `include_snippets = false`.
For `project_context_search`, `kinds` selects memory entity kinds in the memory domain
and repository node kinds in the repository domain. In `all`, recognized memory kinds
filter memory; other kind tokens filter repository nodes independently.

Optional external peers use `mcp_config_file` in provider settings and the existing
[bounded MCP client](ferrus-nano-mcp.md). Managed-binding peers are omitted before
discovery and proxy setup. No Ferrus MCP loopback or remote graph/memory client is
initialized implicitly. `native_context_enabled` and `working_set_enabled` retain their
provider-file switches; an explicitly requested index build is independent of them.

## Verification

`tests/nano_standalone.rs` drives the executable with an offline streaming provider,
native tools, explicit graph/memory sidecars, and an optional external MCP fixture.
It covers unregistered Git/non-Git directories, storage binding, continuation, unknown
effects, and absence of orchestration state. Live model validation remains opt-in.
9 changes: 5 additions & 4 deletions docs/milestones.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Last reviewed against the repository: 2026-09-07.
| Spec closure and project memory | Local baseline implemented | Outcome archival, curated memory indexing, revision-pinned queries, and evidence-backed repository links exist. Raw runtime bodies are excluded from default ingestion. |
| Repository graph and indexed context | Local baseline implemented | Optional SQLite sidecar, incremental extraction, bounded CLI/MCP retrieval, task overlays, and frozen review views. Rust/Cargo and generic file structure are supported. |
| Distributed context data plane | Prototype implemented | Opt-in contracts and local prototype adapters for authorized jobs, encrypted storage, publication, queries, and maintenance. No deployed remote service is implied. |
| Ferrus nano-agent | Headless Executor implemented; release validation pending | #73-#80 add native binding, bounded sessions and tools, an opt-in Chat Completions provider, graph/memory context, managed lifecycle, and CLI/HQ launch. Live model validation, release gates, interactive UI, and standalone delivery remain pending. |
| Ferrus nano-agent | Headless Executor, HQ conversations, and standalone host implemented | #73-#87 deliver native tools/context, managed lifecycle/recovery, evaluation tooling, HQ interaction, and `ferrus-nano`. Comparative live validation, standalone UI, and additional roles remain pending. |

## Milestone 1: Windows Support

Expand Down Expand Up @@ -180,6 +180,7 @@ Implemented foundation:
- external stdio MCP through neva and guarded crash resume with effect reconciliation;
- native HQ conversation with queued steering, cancellation, scoped questions, bounded previews,
and reconnect from the durable journal ([contract](ferrus-nano-interactive.md));
- standalone headless host/binary without registration or orchestration state, explicit local context, and guarded continuation ([contract](ferrus-nano-standalone.md));
- regression coverage for bindings, lease ownership, MCP parity, engine limits, effect ordering, journal recovery, and offline provider protocols. The live provider smoke test remains opt-in.

Delivery is tracked in [Ferrus nano-agents](https://github.com/ferrus-dev/ferrus/milestone/6).
Expand All @@ -192,7 +193,7 @@ contains one issue per planned PR, dependencies, and acceptance criteria:
| N2: context efficiency | #81, #82, and #85 implemented | Comparative context-quality and cache measurements |
| N3: reliability and extensions | #83-#85 implemented | Comparative live evaluation |
| N4: interactive HQ | #86 implemented | Live interactive Executor validation |
| N5: standalone | #87-#88 | Standalone host/binary and shared UI |
| N5: standalone | #87 implemented; #88 pending | Shared standalone UI |
| N5: additional roles | #89-#90 | Supervisor planning/spec/archive, Reviewer, and Consultant |

Definition of done for the first headless release:
Expand All @@ -202,7 +203,7 @@ Definition of done for the first headless release:
- a fixed task suite measures quality, token use, cost, and elapsed time against external integrations.

Lower cost, higher determinism, and better throughput are hypotheses until evaluated. Interactive
HQ is available; standalone delivery remains a planned extension.
HQ and headless standalone are available; standalone interactive UI remains planned.

## Supporting Tracks

Expand Down Expand Up @@ -272,7 +273,7 @@ See [project memory](project_memory/project-memory.md), [architecture](project_m
3. Close real Windows agent-loop validation and support-documentation gaps.
4. Define task dependency, decomposition, conflict-routing, and partial-failure policies beyond current milestone scheduling.
5. Improve runtime history, archive inspection/export, and explicit external-backend capability metadata.
6. Add nano interaction and standalone delivery (#86-#88), then additional roles (#89-#90).
6. Add standalone interactive UI (#88), then additional roles (#89-#90).
7. Evolve the distributed prototype only through explicit deployment and security/operational acceptance gates.

## Non-goals for now
Expand Down
7 changes: 7 additions & 0 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,13 @@ function Install-Binary {
}

Copy-Item $binPath $destination -Force
# Older releases contain only ferrus; install the companion when present.
$nanoPath = Join-Path $TempDir "ferrus-$Target\ferrus-nano.exe"
if (Test-Path $nanoPath) {
Copy-Item $nanoPath (Join-Path $InstallDir "ferrus-nano.exe") -Force
} elseif (Test-Path (Join-Path $InstallDir "ferrus-nano.exe")) {
Write-Warning "this release does not include ferrus-nano; existing $(Join-Path $InstallDir 'ferrus-nano.exe') was retained and may be incompatible. Remove it or install a release that includes Nano."
}
}

function Print-Success {
Expand Down
7 changes: 7 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,13 @@ install_binary() {

warn_existing_install
install "$BIN_PATH" "$INSTALL_DIR/ferrus"
# Older releases contain only ferrus; install the companion when present.
nano_path="$TMP_DIR/ferrus-${TARGET}/ferrus-nano"
if [ -f "$nano_path" ]; then
install "$nano_path" "$INSTALL_DIR/ferrus-nano"
elif [ -e "$INSTALL_DIR/ferrus-nano" ]; then
echo "warning: this release does not include ferrus-nano; existing $INSTALL_DIR/ferrus-nano was retained and may be incompatible. Remove it or install a release that includes Nano." >&2
fi
Comment thread
RomanEmreis marked this conversation as resolved.
}

print_success() {
Expand Down
Loading
Loading