sf snapshot: pre-dispatch, uncommitted changes after 47m inactivity

This commit is contained in:
Mikael Hugo 2026-04-30 20:21:12 +02:00
parent 8e4081e6f1
commit b43bf6991e
68 changed files with 346 additions and 384 deletions

View file

@ -149,7 +149,7 @@ The codebase is organized into these areas. All are open to contributions:
| MCP server | `packages/mcp-server` | Project state tools and MCP protocol |
| SF extension | `src/resources/extensions/sf/` | SF workflow — RFC required for auto-mode |
| Other extensions | `src/resources/extensions/` | Browser, search, voice, MCP client, etc. |
| Native engine | `native/` | Rust N-API modules (grep, git, AST, etc.) |
| Native engine | `rust-engine/` | Rust N-API modules (grep, git, AST, etc.) |
| VS Code extension | `vscode-extension/` | Chat participant, sidebar, RPC integration |
| Web interface | `web/` | Browser-based dashboard |
| CI/Build | `.github/`, `scripts/` | Workflows, build scripts |

View file

@ -6,7 +6,7 @@
"useIgnoreFile": true
},
"files": {
"includes": ["**", "!!**/dist", "!!**/dist-test", "!!**/native/npm"]
"includes": ["**", "!!**/dist", "!!**/dist-test", "!!**/rust-engine/npm"]
},
"formatter": {
"enabled": true,

View file

@ -38,7 +38,7 @@ Design documents, ADRs, and internal references. Located in [`dev/`](./dev/).
| Guide | Description |
|-------|-------------|
| [Architecture Overview](./dev/architecture.md) | System design, extension model, state-on-disk, and dispatch pipeline |
| [Native Engine](../native/README.md) | Rust N-API modules for performance-critical operations |
| [Native Engine](../rust-engine/README.md) | Rust N-API modules for performance-critical operations |
| [ADR-001: Branchless Worktree Architecture](./dev/ADR-001-branchless-worktree-architecture.md) | Decision record for the v2.14 git architecture |
| [ADR-003: Pipeline Simplification](./dev/ADR-003-pipeline-simplification.md) | Research merged into planning, mechanical completion (v2.30) |
| [ADR-004: Capability-Aware Model Routing](./dev/ADR-004-capability-aware-model-routing.md) | Extend routing from tier/cost selection to task-capability matching |

View file

@ -25,7 +25,7 @@ The question the SPEC retarget didn't have to answer: **now that this much is he
- **sf core (TypeScript on pi-mono): unchanged.** SPEC §1 retarget rationale stands. Pi-mono SDK alignment, MCP-server story, ~200+ TS files, real production users — none of it justifies a 36 month rewrite.
- **New services: Go on Charm, comprehensively.** sf-worker (ADR-013), Singularity Knowledge + Agent Platform (ADR-014), flight recorder (ADR-015), Charm TUI client (ADR-017) — all in Go using the Charm ecosystem.
- **Native engine (Rust): permanent.** ~11k LOC in `native/` (git, text, forge_parser, grep, highlight, ast, diff, etc.) is best-of-breed and not re-implementable in Go without losing performance. Bindings (napi-rs from TS today; cgo from Go for new services if needed) flex per consumer.
- **Native engine (Rust): permanent.** ~11k LOC in `rust-engine/` (git, text, forge_parser, grep, highlight, ast, diff, etc.) is best-of-breed and not re-implementable in Go without losing performance. Bindings (napi-rs from TS today; cgo from Go for new services if needed) flex per consumer.
- **Pony adoption: now, not deferred.** Reversed from initial conservative stance. Adopting pony from day one in Phase-3 admin surfaces (Singularity Memory admin UI, future audit dashboards) — admin tolerates churn better than user-facing surfaces, and the foundation bet pays back if pony stabilises.
- **Other `charmbracelet/x/*` packages: adopted comprehensively.** When a new Go service needs a primitive (image rendering, session recording, pty, editor, input handling), use the `x/*` package. Don't reinvent.
- **Re-evaluation trigger: 12 months from first Go service in production.** If >50% of *new* sf code lands in Go services, the question of consolidating sf core becomes worth re-asking. Until then, polyglot is the right cost shape.

View file

@ -847,55 +847,55 @@
---
## native/ — Rust Engine
## rust-engine/ — Rust Engine
| File | System Label(s) | Description |
|------|-----------------|-------------|
| native/crates/engine/src/lib.rs | Native/Rust Tools | N-API entry point exposing all Rust modules |
| native/crates/engine/src/grep.rs | File Search, Native/Rust Tools | Ripgrep-backed regex search with context/globbing |
| native/crates/engine/src/glob.rs | File Search, Native/Rust Tools | Glob-pattern FS discovery with gitignore + scan cache |
| native/crates/engine/src/fd.rs | File Search, Native/Rust Tools | Fuzzy file discovery for autocomplete/@-mentions |
| native/crates/engine/src/highlight.rs | Syntax Highlighting, Native/Rust Tools | Syntect-backed ANSI syntax highlighting |
| native/crates/engine/src/ast.rs | AST, Native/Rust Tools | Linker shim for AST N-API registrations |
| native/crates/engine/src/diff.rs | Text Processing, Native/Rust Tools | Fuzzy matching, Unicode normalization, unified diffs |
| native/crates/engine/src/image.rs | Image Processing, Native/Rust Tools | Image decode/encode and resize |
| native/crates/engine/src/html.rs | Text Processing, Native/Rust Tools | HTML to Markdown conversion |
| native/crates/engine/src/text.rs | Text Processing, Native/Rust Tools | ANSI-aware text measurement and slicing |
| native/crates/engine/src/truncate.rs | Text Processing, Native/Rust Tools | Line-boundary-aware output truncation |
| native/crates/engine/src/ps.rs | Native/Rust Tools | Cross-platform process tree management |
| native/crates/engine/src/clipboard.rs | Native/Rust Tools | Clipboard read/write for text and images |
| native/crates/engine/src/json_parse.rs | Text Processing, Native/Rust Tools | Streaming JSON parser with partial recovery |
| native/crates/engine/src/sf_parser.rs | SF Workflow, Native/Rust Tools | .sf/ directory file parser (markdown, frontmatter) |
| native/crates/engine/src/ttsr.rs | TTSR, Native/Rust Tools | TTSR regex engine with compiled RegexSet |
| native/crates/engine/src/stream_process.rs | Text Processing, Native/Rust Tools | Bash stream processor (UTF-8, ANSI strip, binary) |
| native/crates/engine/src/xxhash.rs | Native/Rust Tools | xxHash32 for hashline edit tool |
| native/crates/engine/src/git.rs | Native/Rust Tools | Native git operations via libgit2 |
| native/crates/engine/src/fs_cache.rs | File Search, Native/Rust Tools | TTL-based FS scan cache with explicit invalidation |
| native/crates/engine/src/glob_util.rs | File Search, Native/Rust Tools | Shared glob-pattern helpers |
| native/crates/engine/src/task.rs | Native/Rust Tools | Blocking work on libuv thread pool with cancellation |
| native/crates/engine/build.rs | Build System | Cargo build script for napi-build compilation |
| native/crates/grep/src/lib.rs | File Search, Native/Rust Tools | Ripgrep search library (in-memory and on-disk) |
| native/crates/ast/src/lib.rs | AST, Native/Rust Tools | AST-aware structural search and rewrite engine |
| native/crates/ast/src/ast.rs | AST, Native/Rust Tools | ast-grep integration for structural code search |
| native/crates/ast/src/language/mod.rs | AST, Native/Rust Tools | Vendored language defs and tree-sitter bindings |
| native/crates/ast/src/language/parsers.rs | AST, Native/Rust Tools | Pre-compiled tree-sitter parsers (50+ languages) |
| rust-engine/crates/engine/src/lib.rs | Native/Rust Tools | N-API entry point exposing all Rust modules |
| rust-engine/crates/engine/src/grep.rs | File Search, Native/Rust Tools | Ripgrep-backed regex search with context/globbing |
| rust-engine/crates/engine/src/glob.rs | File Search, Native/Rust Tools | Glob-pattern FS discovery with gitignore + scan cache |
| rust-engine/crates/engine/src/fd.rs | File Search, Native/Rust Tools | Fuzzy file discovery for autocomplete/@-mentions |
| rust-engine/crates/engine/src/highlight.rs | Syntax Highlighting, Native/Rust Tools | Syntect-backed ANSI syntax highlighting |
| rust-engine/crates/engine/src/ast.rs | AST, Native/Rust Tools | Linker shim for AST N-API registrations |
| rust-engine/crates/engine/src/diff.rs | Text Processing, Native/Rust Tools | Fuzzy matching, Unicode normalization, unified diffs |
| rust-engine/crates/engine/src/image.rs | Image Processing, Native/Rust Tools | Image decode/encode and resize |
| rust-engine/crates/engine/src/html.rs | Text Processing, Native/Rust Tools | HTML to Markdown conversion |
| rust-engine/crates/engine/src/text.rs | Text Processing, Native/Rust Tools | ANSI-aware text measurement and slicing |
| rust-engine/crates/engine/src/truncate.rs | Text Processing, Native/Rust Tools | Line-boundary-aware output truncation |
| rust-engine/crates/engine/src/ps.rs | Native/Rust Tools | Cross-platform process tree management |
| rust-engine/crates/engine/src/clipboard.rs | Native/Rust Tools | Clipboard read/write for text and images |
| rust-engine/crates/engine/src/json_parse.rs | Text Processing, Native/Rust Tools | Streaming JSON parser with partial recovery |
| rust-engine/crates/engine/src/sf_parser.rs | SF Workflow, Native/Rust Tools | .sf/ directory file parser (markdown, frontmatter) |
| rust-engine/crates/engine/src/ttsr.rs | TTSR, Native/Rust Tools | TTSR regex engine with compiled RegexSet |
| rust-engine/crates/engine/src/stream_process.rs | Text Processing, Native/Rust Tools | Bash stream processor (UTF-8, ANSI strip, binary) |
| rust-engine/crates/engine/src/xxhash.rs | Native/Rust Tools | xxHash32 for hashline edit tool |
| rust-engine/crates/engine/src/git.rs | Native/Rust Tools | Native git operations via libgit2 |
| rust-engine/crates/engine/src/fs_cache.rs | File Search, Native/Rust Tools | TTL-based FS scan cache with explicit invalidation |
| rust-engine/crates/engine/src/glob_util.rs | File Search, Native/Rust Tools | Shared glob-pattern helpers |
| rust-engine/crates/engine/src/task.rs | Native/Rust Tools | Blocking work on libuv thread pool with cancellation |
| rust-engine/crates/engine/build.rs | Build System | Cargo build script for napi-build compilation |
| rust-engine/crates/grep/src/lib.rs | File Search, Native/Rust Tools | Ripgrep search library (in-memory and on-disk) |
| rust-engine/crates/ast/src/lib.rs | AST, Native/Rust Tools | AST-aware structural search and rewrite engine |
| rust-engine/crates/ast/src/ast.rs | AST, Native/Rust Tools | ast-grep integration for structural code search |
| rust-engine/crates/ast/src/language/mod.rs | AST, Native/Rust Tools | Vendored language defs and tree-sitter bindings |
| rust-engine/crates/ast/src/language/parsers.rs | AST, Native/Rust Tools | Pre-compiled tree-sitter parsers (50+ languages) |
## packages/native/src/ — Node.js Rust Bindings
## packages/rust-engine/src/ — Node.js Rust Bindings
| File | System Label(s) | Description |
|------|-----------------|-------------|
| packages/native/src/native.ts | Native/Rust Tools, Node.js Bindings | Native addon loader with platform fallback |
| packages/native/src/grep/index.ts | File Search, Node.js Bindings | Ripgrep wrapper for regex search |
| packages/native/src/fd/index.ts | File Search, Node.js Bindings | Fuzzy file discovery wrapper |
| packages/native/src/highlight/index.ts | Syntax Highlighting, Node.js Bindings | Syntax highlighting wrapper |
| packages/native/src/image/index.ts | Image Processing, Node.js Bindings | Image processing wrapper |
| packages/native/src/html/index.ts | Text Processing, Node.js Bindings | HTML to Markdown wrapper |
| packages/native/src/diff/index.ts | Text Processing, Node.js Bindings | Text diffing wrapper |
| packages/native/src/ps/index.ts | Native/Rust Tools, Node.js Bindings | Process tree management wrapper |
| packages/native/src/truncate/index.ts | Text Processing, Node.js Bindings | Output truncation wrapper |
| packages/native/src/json-parse/index.ts | Text Processing, Node.js Bindings | JSON parsing wrapper |
| packages/native/src/stream-process/index.ts | Text Processing, Node.js Bindings | Stream processing wrapper |
| packages/native/src/ttsr/index.ts | TTSR, Node.js Bindings | TTSR regex engine wrapper |
| packages/rust-engine/src/native.ts | Native/Rust Tools, Node.js Bindings | Native addon loader with platform fallback |
| packages/rust-engine/src/grep/index.ts | File Search, Node.js Bindings | Ripgrep wrapper for regex search |
| packages/rust-engine/src/fd/index.ts | File Search, Node.js Bindings | Fuzzy file discovery wrapper |
| packages/rust-engine/src/highlight/index.ts | Syntax Highlighting, Node.js Bindings | Syntax highlighting wrapper |
| packages/rust-engine/src/image/index.ts | Image Processing, Node.js Bindings | Image processing wrapper |
| packages/rust-engine/src/html/index.ts | Text Processing, Node.js Bindings | HTML to Markdown wrapper |
| packages/rust-engine/src/diff/index.ts | Text Processing, Node.js Bindings | Text diffing wrapper |
| packages/rust-engine/src/ps/index.ts | Native/Rust Tools, Node.js Bindings | Process tree management wrapper |
| packages/rust-engine/src/truncate/index.ts | Text Processing, Node.js Bindings | Output truncation wrapper |
| packages/rust-engine/src/json-parse/index.ts | Text Processing, Node.js Bindings | JSON parsing wrapper |
| packages/rust-engine/src/stream-process/index.ts | Text Processing, Node.js Bindings | Stream processing wrapper |
| packages/rust-engine/src/ttsr/index.ts | TTSR, Node.js Bindings | TTSR regex engine wrapper |
---
@ -964,13 +964,13 @@ Quick lookup: which files are part of each system?
| **Agent Core** | pi-agent-core/src/*, pi-coding-agent/src/core/agent-session.ts, agent-loop.ts, agent.ts, event-bus.ts, sdk.ts |
| **AI Providers** | pi-ai/src/providers/*, pi-ai/src/stream.ts, pi-ai/src/models*.ts |
| **API Routes** | web/app/api/**/*.ts |
| **AST** | native/crates/ast/*, packages/native/src/ast/ |
| **AST** | rust-engine/crates/ast/*, packages/rust-engine/src/ast/ |
| **Async Jobs** | src/resources/extensions/async-jobs/* |
| **Auth / OAuth** | pi-ai/src/utils/oauth/*, src/web/web-auth-storage.ts, core/auth-storage.ts, src/pi-migration.ts, aws-auth/index.ts, web/lib/auth.ts |
| **Auto Engine** | src/resources/extensions/sf/auto*.ts, sf/auto-loop.ts, sf/auto-supervisor.ts, sf/unit-runtime.ts |
| **Bg Shell** | src/resources/extensions/bg-shell/* |
| **Browser Tools** | src/resources/extensions/browser-tools/* |
| **Build System** | scripts/*, native/crates/engine/build.rs |
| **Build System** | scripts/*, rust-engine/crates/engine/build.rs |
| **CLI** | src/cli.ts, src/cli-web-branch.ts, src/help-text.ts, src/update*.ts, pi-coding-agent/src/cli.ts, src/worktree-cli.ts |
| **CMux** | src/resources/extensions/cmux/index.ts |
| **Commands** | sf/commands*.ts, sf/exit-command.ts, sf/undo.ts, sf/kill.ts, pi-coding-agent/src/core/slash-commands.ts |
@ -981,11 +981,11 @@ Quick lookup: which files are part of each system?
| **Event System** | pi-coding-agent/src/core/event-bus.ts, sf/auto-observability.ts |
| **Extension Registry** | src/extension-discovery.ts, src/extension-registry.ts, src/bundled-extension-paths.ts |
| **Extensions** | pi-coding-agent/src/core/extensions/*, src/resource-loader.ts |
| **File Search** | native/crates/engine/src/grep.rs, glob.rs, fd.rs, fs_cache.rs, packages/native/src/grep/*, fd/*, core/tools/grep.ts, find.ts |
| **File Search** | rust-engine/crates/engine/src/grep.rs, glob.rs, fd.rs, fs_cache.rs, packages/rust-engine/src/grep/*, fd/*, core/tools/grep.ts, find.ts |
| **SF Workflow** | src/resources/extensions/sf/* (non-auto), sf/reports.ts, sf/notifications.ts, sf/prompts/*, sf/workflow-templates/* |
| **Google Search** | src/resources/extensions/google-search/index.ts |
| **Headless Mode** | src/headless*.ts |
| **Image Processing** | native/crates/engine/src/image.rs, packages/native/src/image/*, utils/image-*.ts, web/lib/image-utils.ts |
| **Image Processing** | rust-engine/crates/engine/src/image.rs, packages/rust-engine/src/image/*, utils/image-*.ts, web/lib/image-utils.ts |
| **Integration Tests** | tests/**/* |
| **Loader / Bootstrap** | src/loader.ts, src/resource-loader.ts, src/tool-bootstrap.ts, src/bundled-resource-path.ts, sf/bootstrap/* |
| **LSP** | pi-coding-agent/src/core/lsp/* |
@ -995,8 +995,8 @@ Quick lookup: which files are part of each system?
| **Migration** | sf/migrate/*, src/pi-migration.ts, pi-coding-agent/src/migrations.ts, scripts/recover-*.sh |
| **Modes** | pi-coding-agent/src/modes/* |
| **Model System** | pi-coding-agent/src/core/model-*.ts, pi-ai/src/models*.ts, pi-ai/src/api-registry.ts, sf/model-router.ts |
| **Native / Rust Tools** | native/crates/engine/src/* |
| **Node.js Bindings** | packages/native/src/* |
| **Native / Rust Tools** | rust-engine/crates/engine/src/* |
| **Node.js Bindings** | packages/rust-engine/src/* |
| **Onboarding** | src/onboarding.ts, src/wizard.ts, web/components/sf/onboarding/*, web/app/api/onboarding/* |
| **Permissions** | core/extensions/project-trust.ts, core/auth-storage.ts |
| **Remote Questions** | src/resources/extensions/remote-questions/* |
@ -1007,10 +1007,10 @@ Quick lookup: which files are part of each system?
| **State Machine** | sf/state.ts, sf/history.ts, sf/json-persistence.ts, sf/memory-store.ts, sf/reactive-graph.ts, core/agent-session.ts, web/lib/sf-workspace-store.tsx |
| **Studio App** | studio/* |
| **Subagent** | src/resources/extensions/subagent/*, src/resources/agents/* |
| **Syntax Highlighting** | native/crates/engine/src/highlight.rs, packages/native/src/highlight/* |
| **Text Processing** | native/crates/engine/src/diff.rs, html.rs, text.rs, truncate.rs, json_parse.rs, stream_process.rs |
| **Syntax Highlighting** | rust-engine/crates/engine/src/highlight.rs, packages/rust-engine/src/highlight/* |
| **Text Processing** | rust-engine/crates/engine/src/diff.rs, html.rs, text.rs, truncate.rs, json_parse.rs, stream_process.rs |
| **Tool System** | pi-coding-agent/src/core/tools/*, core/bash-executor.ts, core/exec.ts |
| **TTSR** | src/resources/extensions/ttsr/*, native/crates/engine/src/ttsr.rs, packages/native/src/ttsr/* |
| **TTSR** | src/resources/extensions/ttsr/*, rust-engine/crates/engine/src/ttsr.rs, packages/rust-engine/src/ttsr/* |
| **TUI Components** | packages/pi-tui/src/*, pi-coding-agent/src/modes/interactive/components/*, pi-coding-agent/src/modes/interactive/controllers/* |
| **Universal Config** | src/resources/extensions/universal-config/* |
| **Voice** | src/resources/extensions/voice/* |

View file

@ -82,7 +82,7 @@ The `-dev.` prerelease identifier is distinct from the existing `-next.` convent
Dev versions (`@dev` tag) use the native binaries from the most recent stable `build-native.yml` release. The `optionalDependencies` in `package.json` use `>=` ranges, so a `-dev.` version of `sf-run` resolves the latest stable `@sf-build/engine-*` packages from the registry.
If a PR modifies Rust native crate code (`native/` directory), the dev publish will bundle stale native binaries. This is acceptable because:
If a PR modifies Rust native crate code (`rust-engine/` directory), the dev publish will bundle stale native binaries. This is acceptable because:
- Native crate changes are infrequent and always accompanied by a `v*` tag release
- The Test stage validates the installed package works end-to-end
- Full native binary validation happens via `build-native.yml` on the version tag

View file

@ -62,7 +62,7 @@
"test:packages": "node --test packages/pi-coding-agent/dist/core/*.test.js packages/pi-coding-agent/dist/core/tools/spawn-shell-windows.test.js",
"test:marketplace": "node scripts/with-env.mjs SF_TEST_CLONE_MARKETPLACES=1 -- node --import ./src/resources/extensions/sf/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/sf/tests/claude-import-tui.test.ts src/resources/extensions/sf/tests/plugin-importer-live.test.ts src/tests/marketplace-discovery.test.ts",
"test:sf-light": "node --max-old-space-size=2048 --import ./src/resources/extensions/sf/tests/resolve-ts.mjs --experimental-strip-types --test-timeout=30000 --test \"src/resources/extensions/sf/tests/*.test.ts\"",
"test:coverage": "c8 --reporter=text --reporter=lcov --exclude=\"src/resources/extensions/sf/tests/**\" --exclude=\"src/tests/**\" --exclude=\"scripts/**\" --exclude=\"native/**\" --exclude=\"node_modules/**\" --check-coverage --statements=40 --lines=40 --branches=20 --functions=20 node --import ./src/resources/extensions/sf/tests/resolve-ts.mjs --experimental-strip-types --experimental-test-isolation=process --test src/resources/extensions/sf/tests/*.test.ts src/resources/extensions/sf/tests/*.test.mjs src/tests/*.test.ts src/resources/extensions/shared/tests/*.test.ts",
"test:coverage": "c8 --reporter=text --reporter=lcov --exclude=\"src/resources/extensions/sf/tests/**\" --exclude=\"src/tests/**\" --exclude=\"scripts/**\" --exclude=\"rust-engine/**\" --exclude=\"node_modules/**\" --check-coverage --statements=40 --lines=40 --branches=20 --functions=20 node --import ./src/resources/extensions/sf/tests/resolve-ts.mjs --experimental-strip-types --experimental-test-isolation=process --test src/resources/extensions/sf/tests/*.test.ts src/resources/extensions/sf/tests/*.test.mjs src/tests/*.test.ts src/resources/extensions/shared/tests/*.test.ts",
"test:integration": "node --import ./src/resources/extensions/sf/tests/resolve-ts.mjs --experimental-strip-types --test \"src/tests/integration/*.test.ts\" \"src/resources/extensions/sf/tests/integration/*.test.ts\" \"src/resources/extensions/async-jobs/*.test.ts\" \"src/resources/extensions/browser-tools/tests/*.test.mjs\"",
"pretest": "npm run typecheck:extensions",
"test": "npm run test:unit && npm run test:integration",
@ -71,12 +71,12 @@
"test:fixtures:record": "node scripts/with-env.mjs SF_FIXTURE_MODE=record -- node --experimental-strip-types tests/fixtures/record.ts",
"test:live": "node scripts/with-env.mjs SF_LIVE_TESTS=1 -- node --experimental-strip-types tests/live/run.ts",
"test:browser-tools": "node --test src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs src/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs",
"test:native": "node --test packages/native/src/__tests__/grep.test.mjs",
"test:native": "node --test packages/rust-engine/src/__tests__/grep.test.mjs",
"test:secret-scan": "node --import ./src/resources/extensions/sf/tests/resolve-ts.mjs --experimental-strip-types --test src/tests/secret-scan.test.ts",
"secret-scan": "node scripts/secret-scan.mjs",
"secret-scan:install-hook": "node scripts/install-hooks.mjs",
"build:native": "node native/scripts/build.js",
"build:native:dev": "node native/scripts/build.js --dev",
"build:native": "node rust-engine/scripts/build.js",
"build:native:dev": "node rust-engine/scripts/build.js --dev",
"dev": "node scripts/dev.js",
"sf": "node scripts/dev-cli.js",
"sf:web": "npm run build:pi && npm run copy-resources && node scripts/build-web-if-stale.cjs && node scripts/dev-cli.js --web",
@ -86,7 +86,7 @@
"pi:install-global": "node scripts/install-pi-global.js",
"pi:uninstall-global": "node scripts/uninstall-pi-global.js",
"sync-pkg-version": "node scripts/sync-pkg-version.cjs",
"sync-platform-versions": "node native/scripts/sync-platform-versions.cjs",
"sync-platform-versions": "node rust-engine/scripts/sync-platform-versions.cjs",
"validate-pack": "node scripts/validate-pack.js",
"typecheck:extensions": "npm run check:versioned-json && tsc --noEmit --project tsconfig.extensions.json",
"check:versioned-json": "node scripts/check-versioned-json.mjs",

View file

@ -7,8 +7,8 @@
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc -p tsconfig.json",
"build:native": "node ../../native/scripts/build.js",
"build:native:dev": "node ../../native/scripts/build.js --dev",
"build:native": "node ../../rust-engine/scripts/build.js",
"build:native:dev": "node ../../rust-engine/scripts/build.js --dev",
"test": "npm run build:native:dev && node --test src/__tests__/grep.test.mjs src/__tests__/ps.test.mjs src/__tests__/glob.test.mjs src/__tests__/clipboard.test.mjs src/__tests__/highlight.test.mjs src/__tests__/html.test.mjs src/__tests__/text.test.mjs src/__tests__/fd.test.mjs src/__tests__/image.test.mjs"
},
"exports": {

View file

@ -4,8 +4,8 @@
* Locates and loads the compiled Rust N-API addon (`.node` file).
* Resolution order:
* 1. @singularity-forge/engine-{platform} npm optional dependency (production install)
* 2. native/addon/forge_engine.{platform}.node (local release build)
* 3. native/addon/forge_engine.dev.node (local debug build)
* 2. rust-engine/addon/forge_engine.{platform}.node (local release build)
* 3. rust-engine/addon/forge_engine.dev.node (local debug build)
*/
import * as path from "node:path";
@ -16,7 +16,7 @@ import * as path from "node:path";
const _dirname = __dirname;
const _require = require;
const addonDir = path.resolve(_dirname, "..", "..", "..", "native", "addon");
const addonDir = path.resolve(_dirname, "..", "..", "..", "rust-engine", "addon");
const platformTag = `${process.platform}-${process.arch}`;
/** Map Node.js platform/arch to the npm package suffix */
@ -44,7 +44,7 @@ function loadNative(): Record<string, unknown> {
}
}
// 2. Try local release build (native/addon/forge_engine.{platform}.node)
// 2. Try local release build (rust-engine/addon/forge_engine.{platform}.node)
const releasePath = path.join(addonDir, `forge_engine.${platformTag}.node`);
try {
_loadedSuccessfully = true; return _require(releasePath) as Record<string, unknown>;
@ -53,7 +53,7 @@ function loadNative(): Record<string, unknown> {
errors.push(`${releasePath}: ${message}`);
}
// 3. Try local dev build (native/addon/forge_engine.dev.node)
// 3. Try local dev build (rust-engine/addon/forge_engine.dev.node)
const devPath = path.join(addonDir, "forge_engine.dev.node");
try {
_loadedSuccessfully = true; return _require(devPath) as Record<string, unknown>;

View file

@ -208,7 +208,7 @@ export function parseArgs(args: string[], extensionFlags?: Map<string, Extension
}
export function printHelp(): void {
console.log(`${chalk.bold(APP_NAME)} - AI coding assistant with read, bash, edit, write tools
console.log(`${chalk.bold(APP_NAME)} - AI coding assistant with native read/search/edit/write tools
${chalk.bold("Usage:")}
${APP_NAME} [options] [@files...] [messages...]
@ -238,7 +238,7 @@ ${chalk.bold("Options:")}
Supports globs (anthropic/*, *sonnet*) and fuzzy matching
--no-tools Disable all built-in tools
--tools <tools> Comma-separated list of tools to enable (default: read,bash,edit,write)
Available: read, bash, edit, write, lsp, grep, find, ls
Available: read, grep, find, ls, bash, edit, write, lsp
--thinking <level> Set thinking level: off, minimal, low, medium, high, xhigh
--extension, -e <path> Load an extension file (can be used multiple times)
--no-extensions, -ne Disable extension discovery (explicit -e paths still work)
@ -339,7 +339,7 @@ ${chalk.bold("Environment Variables:")}
PI_OFFLINE - Disable startup network operations when set to 1/true/yes
PI_SHARE_VIEWER_URL - Base URL for /share command (default: https://pi.dev/session/)
${chalk.bold("Available Tools (default: read, bash, edit, write):")}
${chalk.bold("Available Tools (default: read, grep, find, ls, bash, edit, write, lsp):")}
read - Read file contents
bash - Execute bash commands
edit - Edit files with find/replace

View file

@ -160,7 +160,7 @@ export interface AgentSessionConfig {
customTools?: ToolDefinition[];
/** Model registry for API key resolution and model discovery */
modelRegistry: ModelRegistry;
/** Initial active built-in tool names. Default: [read, bash, edit, write] */
/** Initial active built-in tool names. Default: [read, grep, find, ls, bash, edit, write, lsp] */
initialActiveToolNames?: string[];
/** Override base tools (useful for custom runtimes). */
baseToolsOverride?: Record<string, AgentTool>;
@ -2233,7 +2233,7 @@ export class AgentSession {
const defaultActiveToolNames = this._baseToolsOverride
? Object.keys(this._baseToolsOverride)
: ["read", "bash", "edit", "write", "lsp"];
: ["read", "grep", "find", "ls", "bash", "edit", "write", "lsp"];
const baseActiveToolNames = options.activeToolNames ?? defaultActiveToolNames;
this._refreshToolRegistry({
activeToolNames: baseActiveToolNames,

View file

@ -94,7 +94,7 @@ export interface CreateAgentSessionOptions {
/** Models available for cycling (Ctrl+P in interactive mode) */
scopedModels?: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;
/** Built-in tools to use. Default: codingTools [read, bash, edit, write] */
/** Built-in tools to use. Default: codingTools [read, grep, find, ls, bash, edit, write, lsp] */
tools?: Tool[];
/** Custom tools to register (in addition to built-in tools). */
customTools?: ToolDefinition[];
@ -313,8 +313,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
const editMode = settingsManager.getEditMode();
const defaultActiveToolNames: ToolName[] = editMode === "hashline"
? ["hashline_read", "bash", "hashline_edit", "write", "lsp"]
: ["read", "bash", "edit", "write", "lsp"];
? ["hashline_read", "grep", "find", "ls", "bash", "hashline_edit", "write", "lsp"]
: ["read", "grep", "find", "ls", "bash", "edit", "write", "lsp"];
const initialActiveToolNames: ToolName[] = options.tools
? options.tools.map((t) => t.name).filter((n): n is ToolName => n in allTools)
: defaultActiveToolNames;

View file

@ -21,7 +21,7 @@ const toolDescriptions: Record<string, string> = {
export interface BuildSystemPromptOptions {
/** Custom system prompt (replaces default). */
customPrompt?: string;
/** Tools to include in prompt. Default: [read, bash, edit, write] */
/** Tools to include in prompt. Default: [read, grep, find, ls, bash, edit, write, lsp] */
selectedTools?: string[];
/** Optional one-line tool snippets keyed by tool name. */
toolSnippets?: Record<string, string>;
@ -149,7 +149,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
// Build tools list based on selected tools.
// Built-ins use toolDescriptions. Custom tools can provide one-line snippets.
const tools = selectedTools || ["read", "bash", "edit", "write"];
const tools = selectedTools || ["read", "grep", "find", "ls", "bash", "edit", "write", "lsp"];
const toolsList =
tools.length > 0
? tools

View file

@ -136,7 +136,7 @@ import { createLspTool, lspTool } from "../lsp/index.js";
export type Tool = AgentTool<any>;
// Default tools for full access mode (using process.cwd())
export const codingTools: Tool[] = [readTool, bashTool, editTool, writeTool];
export const codingTools: Tool[] = [readTool, grepTool, findTool, lsTool, bashTool, editTool, writeTool, lspTool];
// Read-only tools for exploration without modification (using process.cwd())
export const readOnlyTools: Tool[] = [readTool, grepTool, findTool, lsTool];
@ -156,7 +156,16 @@ export const allTools = {
};
// Hashline-mode coding tools — read with hash anchors, edit with hash references
export const hashlineCodingTools: Tool[] = [hashlineReadTool, bashTool, hashlineEditTool, writeTool];
export const hashlineCodingTools: Tool[] = [
hashlineReadTool,
grepTool,
findTool,
lsTool,
bashTool,
hashlineEditTool,
writeTool,
lspTool,
];
export type ToolName = keyof typeof allTools;
@ -173,9 +182,13 @@ export interface ToolsOptions {
export function createCodingTools(cwd: string, options?: ToolsOptions): Tool[] {
return [
createReadTool(cwd, options?.read),
createGrepTool(cwd),
createFindTool(cwd),
createLsTool(cwd),
createBashTool(cwd, options?.bash),
createEditTool(cwd),
createWriteTool(cwd),
createLspTool(cwd),
];
}
@ -211,8 +224,12 @@ export function createAllTools(cwd: string, options?: ToolsOptions): Record<Tool
export function createHashlineCodingTools(cwd: string, options?: ToolsOptions): Tool[] {
return [
createHashlineReadTool(cwd, options?.read),
createGrepTool(cwd),
createFindTool(cwd),
createLsTool(cwd),
createBashTool(cwd, options?.bash),
createHashlineEditTool(cwd),
createWriteTool(cwd),
createLspTool(cwd),
];
}

View file

@ -1,197 +1,190 @@
/**
* Built-in theme definitions.
*
* Each theme is a self-contained record of color values. Variable references
* (e.g. "accent") are resolved against the `vars` map at load time by the
* theme engine in theme.ts.
*
* To add a new built-in theme, add an entry to `builtinThemes` below.
* The default palettes follow the Singularity Engine design system:
* Scandinavian x IBM Carbon, ember-40 accent, warm gray/stone neutrals,
* paper/ink semantics, hard hairlines, and restrained status color.
*/
// Re-use the ThemeJson type from the schema defined in theme.ts.
// We import only the type to avoid circular runtime dependencies.
import type { ThemeJson } from "./theme.js";
// ---------------------------------------------------------------------------
// Dark theme
// ---------------------------------------------------------------------------
const dark: ThemeJson = {
name: "dark",
vars: {
cyan: "#00d7ff",
blue: "#5f87ff",
green: "#b5bd68",
red: "#cc6666",
yellow: "#e6b800",
gray: "#808080",
dimGray: "#666666",
darkGray: "#505050",
accent: "#8abeb7",
selectedBg: "#3a3a4a",
userMsgBg: "#343541",
toolPendingBg: "#282832",
toolSuccessBg: "#283228",
toolErrorBg: "#3c2828",
customMsgBg: "#2d2838",
ember40: "#ff8838",
paper: "#f7f5f1",
paper2: "#efece6",
stone1: "#d4cfc3",
stone2: "#8d877a",
stone3: "#6b6659",
ink1: "#1a1916",
ink2: "#2c2a26",
ink3: "#3d3b36",
green: "#24a148",
red: "#da1e28",
blue: "#4589ff",
yellow: "#f1c21b",
mutedBg: "#22201d",
selectedBg: "#2c2a26",
userMsgBg: "#1a1916",
toolPendingBg: "#1a1916",
toolSuccessBg: "#172018",
toolErrorBg: "#261717",
customMsgBg: "#22201d",
},
colors: {
accent: "accent",
border: "blue",
borderAccent: "cyan",
borderMuted: "darkGray",
accent: "ember40",
border: "stone3",
borderAccent: "ember40",
borderMuted: "ink3",
success: "green",
error: "red",
warning: "yellow",
muted: "gray",
dim: "dimGray",
text: "",
thinkingText: "gray",
warning: "ember40",
muted: "stone2",
dim: "stone3",
text: "paper",
thinkingText: "stone2",
selectedBg: "selectedBg",
userMessageBg: "userMsgBg",
userMessageText: "",
userMessageText: "paper",
customMessageBg: "customMsgBg",
customMessageText: "",
customMessageLabel: "#9575cd",
customMessageText: "paper",
customMessageLabel: "ember40",
toolPendingBg: "toolPendingBg",
toolSuccessBg: "toolSuccessBg",
toolErrorBg: "toolErrorBg",
toolTitle: "",
toolOutput: "gray",
toolTitle: "ember40",
toolOutput: "stone2",
mdHeading: "#f0c674",
mdLink: "#81a2be",
mdLinkUrl: "dimGray",
mdCode: "accent",
mdCodeBlock: "green",
mdCodeBlockBorder: "gray",
mdQuote: "gray",
mdQuoteBorder: "gray",
mdHr: "gray",
mdListBullet: "accent",
mdHeading: "paper",
mdLink: "ember40",
mdLinkUrl: "stone2",
mdCode: "ember40",
mdCodeBlock: "paper2",
mdCodeBlockBorder: "stone3",
mdQuote: "stone1",
mdQuoteBorder: "ember40",
mdHr: "stone3",
mdListBullet: "ember40",
toolDiffAdded: "green",
toolDiffRemoved: "red",
toolDiffContext: "gray",
toolDiffContext: "stone2",
syntaxComment: "#6A9955",
syntaxKeyword: "#569CD6",
syntaxFunction: "#DCDCAA",
syntaxVariable: "#9CDCFE",
syntaxString: "#CE9178",
syntaxNumber: "#B5CEA8",
syntaxType: "#4EC9B0",
syntaxOperator: "#D4D4D4",
syntaxPunctuation: "#D4D4D4",
syntaxComment: "stone2",
syntaxKeyword: "ember40",
syntaxFunction: "paper",
syntaxVariable: "paper2",
syntaxString: "#ffb27a",
syntaxNumber: "#f1c21b",
syntaxType: "#33b1ff",
syntaxOperator: "stone1",
syntaxPunctuation: "stone2",
thinkingOff: "darkGray",
thinkingMinimal: "#6e6e6e",
thinkingLow: "#5f87af",
thinkingMedium: "#81a2be",
thinkingHigh: "#b294bb",
thinkingXhigh: "#d183e8",
thinkingOff: "ink3",
thinkingMinimal: "stone3",
thinkingLow: "stone2",
thinkingMedium: "ember40",
thinkingHigh: "#ffb27a",
thinkingXhigh: "#fff1e6",
bashMode: "green",
},
export: {
pageBg: "#18181e",
cardBg: "#1e1e24",
infoBg: "#3c3728",
pageBg: "#1a1916",
cardBg: "#22201d",
infoBg: "#2c2a26",
},
};
// ---------------------------------------------------------------------------
// Light theme
// ---------------------------------------------------------------------------
const light: ThemeJson = {
name: "light",
vars: {
teal: "#5a8080",
blue: "#547da7",
green: "#588458",
red: "#aa5555",
yellow: "#9a7326",
warning: "#7a5a00",
mediumGray: "#6c6c6c",
dimGray: "#767676",
lightGray: "#b0b0b0",
selectedBg: "#d0d0e0",
userMsgBg: "#e8e8e8",
toolPendingBg: "#e8e8f0",
toolSuccessBg: "#e8f0e8",
toolErrorBg: "#f0e8e8",
customMsgBg: "#ede7f6",
ember40: "#ff8838",
paper: "#f7f5f1",
paper2: "#efece6",
paper3: "#e6e2da",
stone1: "#d4cfc3",
stone2: "#8d877a",
stone3: "#6b6659",
ink1: "#1a1916",
ink2: "#3d3b36",
green: "#24a148",
red: "#da1e28",
blue: "#4589ff",
yellow: "#f1c21b",
selectedBg: "#efece6",
userMsgBg: "#ffffff",
toolPendingBg: "#ffffff",
toolSuccessBg: "#f0f6ef",
toolErrorBg: "#fff1ee",
customMsgBg: "#efece6",
},
colors: {
accent: "teal",
border: "blue",
borderAccent: "teal",
borderMuted: "lightGray",
accent: "ember40",
border: "stone3",
borderAccent: "ember40",
borderMuted: "stone1",
success: "green",
error: "red",
warning: "warning",
muted: "mediumGray",
dim: "dimGray",
text: "",
thinkingText: "mediumGray",
warning: "ember40",
muted: "stone3",
dim: "stone2",
text: "ink1",
thinkingText: "stone3",
selectedBg: "selectedBg",
userMessageBg: "userMsgBg",
userMessageText: "",
userMessageText: "ink1",
customMessageBg: "customMsgBg",
customMessageText: "",
customMessageLabel: "#7e57c2",
customMessageText: "ink1",
customMessageLabel: "ember40",
toolPendingBg: "toolPendingBg",
toolSuccessBg: "toolSuccessBg",
toolErrorBg: "toolErrorBg",
toolTitle: "",
toolOutput: "mediumGray",
toolTitle: "ember40",
toolOutput: "stone3",
mdHeading: "yellow",
mdLink: "blue",
mdLinkUrl: "dimGray",
mdCode: "teal",
mdCodeBlock: "green",
mdCodeBlockBorder: "mediumGray",
mdQuote: "mediumGray",
mdQuoteBorder: "mediumGray",
mdHr: "mediumGray",
mdListBullet: "green",
mdHeading: "ink1",
mdLink: "ember40",
mdLinkUrl: "stone3",
mdCode: "ember40",
mdCodeBlock: "ink2",
mdCodeBlockBorder: "stone1",
mdQuote: "stone3",
mdQuoteBorder: "ember40",
mdHr: "stone1",
mdListBullet: "ember40",
toolDiffAdded: "green",
toolDiffRemoved: "red",
toolDiffContext: "mediumGray",
toolDiffContext: "stone3",
syntaxComment: "#008000",
syntaxKeyword: "#0000FF",
syntaxFunction: "#795E26",
syntaxVariable: "#001080",
syntaxString: "#A31515",
syntaxNumber: "#098658",
syntaxType: "#267F99",
syntaxOperator: "#000000",
syntaxPunctuation: "#000000",
syntaxComment: "stone3",
syntaxKeyword: "#b5500f",
syntaxFunction: "ink1",
syntaxVariable: "ink2",
syntaxString: "#8a3d0a",
syntaxNumber: "#87620a",
syntaxType: "#3a5c8c",
syntaxOperator: "ink2",
syntaxPunctuation: "stone3",
thinkingOff: "lightGray",
thinkingMinimal: "#767676",
thinkingLow: "blue",
thinkingMedium: "teal",
thinkingHigh: "#875f87",
thinkingXhigh: "#8b008b",
thinkingOff: "stone1",
thinkingMinimal: "stone2",
thinkingLow: "stone3",
thinkingMedium: "ember40",
thinkingHigh: "#e56a1a",
thinkingXhigh: "#8a3d0a",
bashMode: "green",
},
export: {
pageBg: "#f8f8f8",
pageBg: "#f7f5f1",
cardBg: "#ffffff",
infoBg: "#fffae6",
infoBg: "#efece6",
},
};
// ---------------------------------------------------------------------------
// Export
// ---------------------------------------------------------------------------
export const builtinThemes: Record<string, ThemeJson> = { dark, light };

View file

@ -7,7 +7,7 @@ Rust N-API addon providing high-performance native modules for SF.
```
JS (packages/native) -> N-API -> Rust crates
native/crates/
rust-engine/crates/
├── engine/ (N-API bindings, cdylib — 20+ modules)
├── grep/ (ripgrep internals, pure Rust lib)
└── ast/ (ast-grep structural search)
@ -30,7 +30,7 @@ npm run build:native
npm run build:native:dev
```
The build script compiles the Rust code and copies the `.node` shared library to `native/addon/`.
The build script compiles the Rust code and copies the `.node` shared library to `rust-engine/addon/`.
## Test
@ -153,7 +153,7 @@ xxHash hashing. Provides fast, non-cryptographic hashing via the xxHash algorith
## Adding New Modules
1. Create a new crate in `native/crates/` (pure Rust library)
2. Add N-API bindings in `native/crates/engine/src/`
3. Add TypeScript wrapper in `packages/native/src/`
1. Create a new crate in `rust-engine/crates/` (pure Rust library)
2. Add N-API bindings in `rust-engine/crates/engine/src/`
3. Add TypeScript wrapper in `package./rust-engine/src/`
4. Add the crate to `engine/Cargo.toml` dependencies

View file

@ -59,7 +59,7 @@ for (const name of workspacePackages) {
}
// 3. Sync platform package versions (reads from root package.json)
execSync("node native/scripts/sync-platform-versions.cjs", { cwd: root, stdio: "inherit" });
execSync("node rust-engine/scripts/sync-platform-versions.cjs", { cwd: root, stdio: "inherit" });
// 4. Sync pkg/package.json (reads from pi-coding-agent)
execSync("node scripts/sync-pkg-version.cjs", { cwd: root, stdio: "inherit" });

View file

@ -88,7 +88,7 @@ for (const dir of packageDirs) {
if (linked > 0) process.stderr.write(` Linked ${linked} workspace package${linked !== 1 ? 's' : ''}\n`)
if (copied > 0) process.stderr.write(` Copied ${copied} workspace package${copied !== 1 ? 's' : ''} (symlinks unavailable)\n`)
// Platform-specific native engine packages live under native/npm/<suffix>/, not packages/.
// Platform-specific native engine packages live under rust-engine/npm/<suffix>/, not packages/.
// Wire them into node_modules/@singularity-forge/ so native.ts can require() them without
// a registry install. Only link platforms where the binary (forge_engine.node) is present.
const nativeNpmDir = join(root, 'native', 'npm')

View file

@ -5,11 +5,62 @@ import type {
} from "@singularity-forge/pi-coding-agent";
import { truncateToWidth, visibleWidth } from "@singularity-forge/pi-tui";
import { refreshGitStatus } from "./git.js";
import {
renderPowerline,
renderPowerlineRight,
type Segment,
} from "./powerline.js";
const RESET = "\x1b[0m";
const BOLD = "\x1b[1m";
const SE = {
ember40: "#ff8838",
gray60: "#8d877a",
stone60: "#6b6659",
paper: "#f7f5f1",
success: "#24a148",
error: "#da1e28",
} as const;
type Tone = "muted" | "accent" | "text" | "success" | "warning" | "error";
function hexToRgb(hex: string): { r: number; g: number; b: number } {
const cleaned = hex.replace("#", "");
return {
r: parseInt(cleaned.slice(0, 2), 16),
g: parseInt(cleaned.slice(2, 4), 16),
b: parseInt(cleaned.slice(4, 6), 16),
};
}
function ansiFg(hex: string, text: string, bold = false): string {
const { r, g, b } = hexToRgb(hex);
return `\x1b[${bold ? "1;" : ""}38;2;${r};${g};${b}m${text}${RESET}`;
}
function toneHex(tone: Tone): string {
switch (tone) {
case "accent":
case "warning":
return SE.ember40;
case "success":
return SE.success;
case "error":
return SE.error;
case "text":
return SE.paper;
default:
return SE.gray60;
}
}
function chip(label: string, value: string, tone: Tone = "text"): string {
return `${ansiFg(SE.gray60, `${label} `)}${ansiFg(toneHex(tone), value)}`;
}
function join(parts: string[]): string {
return parts.filter(Boolean).join(ansiFg(SE.stone60, " | "));
}
function shorten(text: string, max: number): string {
return text.length > max ? `${text.slice(0, Math.max(0, max - 3))}...` : text;
}
function getSessionStats(ctx: ExtensionContext) {
let cost = 0;
@ -36,7 +87,7 @@ function getSessionStats(ctx: ExtensionContext) {
}
export function renderFooter(
theme: Theme,
_theme: Theme,
footerData: ReadonlyFooterDataProvider,
ctx: ExtensionContext,
width: number,
@ -44,95 +95,57 @@ export function renderFooter(
const git = refreshGitStatus(process.cwd());
const { cost, cxPct } = getSessionStats(ctx);
const leftSegments: Segment[] = [];
if (git.branch) {
const dirtyIcon = git.dirty ? "dirty" : git.untracked ? "new" : "clean";
leftSegments.push({
text: `repo ${git.branch}`,
fg: "white",
bg: git.dirty || git.untracked ? "brightBlack" : "blue",
bold: true,
});
leftSegments.push({
text: dirtyIcon,
fg: git.dirty || git.untracked ? "black" : "white",
bg: git.dirty || git.untracked ? "yellow" : "green",
bold: true,
});
if (git.added || git.deleted) {
const diffText = `Δ +${git.added}/-${git.deleted}`;
leftSegments.push({ text: diffText, fg: "white", bg: "brightBlack" });
}
if (git.lastCommit) {
const msg =
git.lastCommit.message.length > 20
? git.lastCommit.message.slice(0, 19) + "…"
: git.lastCommit.message;
leftSegments.push({
text: `${git.lastCommit.timeAgo} · ${msg}`,
fg: "white",
bg: "brightBlack",
});
}
const leftParts: string[] = [];
if (git.repo) {
leftParts.push(ansiFg(SE.ember40, git.repo, true));
} else {
leftSegments.push({ text: "SF", fg: "white", bg: "blue", bold: true });
leftParts.push(`${BOLD}${ansiFg(SE.ember40, "SF")}`);
}
if (git.branch) {
leftParts.push(chip("branch", git.branch, "muted"));
const state = git.dirty ? "dirty" : git.untracked ? "new" : "clean";
leftParts.push(chip("state", state, state === "clean" ? "success" : "warning"));
if (git.added || git.deleted) {
leftParts.push(chip("diff", `+${git.added}/-${git.deleted}`, "warning"));
}
if (git.ahead || git.behind) {
leftParts.push(chip("sync", `${git.ahead} ahead ${git.behind} behind`, "warning"));
}
if (git.lastCommit) {
leftParts.push(chip("last", `${git.lastCommit.timeAgo} ${shorten(git.lastCommit.message, 26)}`, "muted"));
}
}
// Extension statuses
const statuses = Array.from(footerData.getExtensionStatuses().entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([, text]) => text.trim())
.filter(Boolean);
if (statuses.length) {
leftSegments.push({
text: `status ${statuses.join(" ")}`,
fg: "white",
bg: "brightBlack",
});
leftParts.push(chip("status", statuses.join(" "), "accent"));
}
const rightSegments: Segment[] = [];
const rightParts: string[] = [];
if (ctx.model) {
rightSegments.push({
text: `model ${ctx.model.provider}/${ctx.model.id}`,
fg: "white",
bg: "blue",
});
rightParts.push(chip("model", `${ctx.model.provider}/${ctx.model.id}`, "text"));
}
if (cost > 0) {
rightSegments.push({
text: `spent $${cost.toFixed(2)}`,
fg: "black",
bg: "yellow",
});
rightParts.push(chip("spent", `$${cost.toFixed(2)}`, "warning"));
}
const cxTone: Tone = cxPct >= 85 ? "error" : cxPct >= 60 ? "warning" : "success";
rightParts.push(chip("ctx", `${Math.round(cxPct)}%`, cxTone));
let rightLine = join(rightParts);
const maxRightWidth = Math.max(16, Math.floor(width * 0.55));
if (visibleWidth(rightLine) > maxRightWidth) {
rightLine = truncateToWidth(rightLine, maxRightWidth, ansiFg(SE.gray60, "..."));
}
const cxColor = cxPct >= 85 ? "red" : cxPct >= 60 ? "yellow" : "green";
rightSegments.push({
text: `ctx ${Math.round(cxPct)}%`,
fg: cxPct >= 85 ? "white" : "black",
bg: cxColor,
});
// Reserve space for right side
const rightLine = renderPowerlineRight(rightSegments, width, theme);
const rightWidth = visibleWidth(rightLine);
const leftLine = renderPowerline(
leftSegments,
Math.max(1, width - rightWidth),
theme,
);
const leftWidth = visibleWidth(leftLine);
// Compose: left powerline + spaces to align + right powerline
const gap = Math.max(0, width - leftWidth - rightWidth);
const leftBudget = Math.max(1, width - rightWidth - 2);
const leftLine = truncateToWidth(join(leftParts), leftBudget, ansiFg(SE.gray60, "..."));
const gap = Math.max(1, width - visibleWidth(leftLine) - rightWidth);
const line = leftLine + " ".repeat(gap) + rightLine;
return [truncateToWidth(line, width, theme.fg("dim", "…"))];
return [truncateToWidth(line, width, ansiFg(SE.gray60, "..."))];
}

View file

@ -1,6 +1,8 @@
import { execFileSync } from "node:child_process";
import { basename } from "node:path";
export interface GitStatus {
repo: string | null;
branch: string | null;
dirty: boolean;
untracked: boolean;
@ -15,6 +17,20 @@ export interface GitStatus {
let cache: GitStatus | null = null;
let lastFetch = 0;
function getRepoName(cwd: string): string | null {
try {
const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
cwd,
encoding: "utf-8",
stdio: ["pipe", "pipe", "ignore"],
timeout: 1500,
}).trim();
return root ? basename(root) : basename(cwd) || null;
} catch {
return basename(cwd) || null;
}
}
function getLastCommit(
cwd: string,
): { timeAgo: string; message: string } | null {
@ -74,6 +90,7 @@ export function refreshGitStatus(cwd: string): GitStatus {
if (now - lastFetch < 400 && cache) return cache;
lastFetch = now;
const repo = getRepoName(cwd);
let branch: string | null = null;
try {
branch =
@ -85,6 +102,7 @@ export function refreshGitStatus(cwd: string): GitStatus {
}).trim() || null;
} catch {
cache = {
repo,
branch: null,
dirty: false,
untracked: false,
@ -136,9 +154,10 @@ export function refreshGitStatus(cwd: string): GitStatus {
const diff = getDiffStats(cwd);
const lastCommit = getLastCommit(cwd);
cache = { branch, dirty, untracked, ahead, behind, ...diff, lastCommit };
cache = { repo, branch, dirty, untracked, ahead, behind, ...diff, lastCommit };
} catch {
cache = {
repo,
branch,
dirty: false,
untracked: false,

View file

@ -75,7 +75,7 @@ For native (Rust) changes:
```bash
npm run build:native
ldd native/npm/linux-x64-gnu/forge_engine.node | grep -E "not found" || echo "OK"
ldd rust-engine/npm/linux-x64-gnu/forge_engine.node | grep -E "not found" || echo "OK"
```
## Git Workflow

View file

@ -59,7 +59,7 @@ For LLM/provider/transport bugs, capture all of:
For native-engine bugs (`forge_engine.node`):
```bash
ldd native/npm/linux-x64-gnu/forge_engine.node 2>&1 | grep -E "not found|missing"
ldd rust-engine/npm/linux-x64-gnu/forge_engine.node 2>&1 | grep -E "not found|missing"
```
When evidence collection splits into independent streams, fan out with parallel subagents (`Explore` for repo search, `Plan` for design analysis). Keep the root-cause + repro doctrine in this skill.

View file

@ -16,7 +16,7 @@
"@singularity-forge/pi-agent-core": ["packages/pi-agent-core/src/index.ts"],
"@singularity-forge/pi-tui": ["packages/pi-tui/src/index.ts"],
"@singularity-forge/native": ["packages/native/src/index.ts"],
"@singularity-forge/native/*": ["packages/native/src/*/index.ts"],
"@singularity-forge/native/*": ["packages/rust-engine/src/*/index.ts"],
"@singularity-forge/mcp-server": ["packages/mcp-server/src/index.ts"],
"@singularity-forge/rpc-client": ["packages/rpc-client/src/index.ts"]
}

View file

@ -1,80 +0,0 @@
import { NextResponse, type NextRequest } from "next/server"
/**
* Next.js middleware validates bearer token and origin on all API routes.
*
* The SF_WEB_AUTH_TOKEN env var is set at server launch. Every /api/* request
* must carry a matching `Authorization: Bearer <token>` header. EventSource
* (SSE) connections may use the `_token` query parameter instead since the
* EventSource API cannot set custom headers.
*
* Additionally, if an `Origin` header is present, it must match the expected
* localhost origin to prevent cross-site request forgery.
*/
export function middleware(request: NextRequest): NextResponse | undefined {
const { pathname } = request.nextUrl
// Only gate API routes
if (!pathname.startsWith("/api/")) return NextResponse.next()
const expectedToken = process.env.SF_WEB_AUTH_TOKEN
if (!expectedToken) {
// If no token was configured (e.g. dev mode without launch harness),
// allow everything — the server didn't opt into auth.
return NextResponse.next()
}
// ── Origin / CORS check ────────────────────────────────────────────
const origin = request.headers.get("origin")
if (origin) {
const host = process.env.SF_WEB_HOST || "127.0.0.1"
const port = process.env.SF_WEB_PORT || "3000"
// Default: localhost origin for the launched host:port
const allowed = new Set([`http://${host}:${port}`])
// SF_WEB_ALLOWED_ORIGINS lets users whitelist additional origins for
// secure tunnel setups (Tailscale Serve, Cloudflare Tunnel, ngrok, etc.)
const extra = process.env.SF_WEB_ALLOWED_ORIGINS
if (extra) {
for (const entry of extra.split(",")) {
const trimmed = entry.trim()
if (trimmed) allowed.add(trimmed)
}
}
if (!allowed.has(origin)) {
return NextResponse.json(
{ error: "Forbidden: origin mismatch" },
{ status: 403 },
)
}
}
// ── Bearer token check ─────────────────────────────────────────────
let token: string | null = null
// 1. Authorization header (preferred)
const authHeader = request.headers.get("authorization")
if (authHeader?.startsWith("Bearer ")) {
token = authHeader.slice(7)
}
// 2. Query parameter fallback for EventSource / SSE
if (!token) {
token = request.nextUrl.searchParams.get("_token")
}
if (!token || token !== expectedToken) {
return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401 },
)
}
return NextResponse.next()
}
export const config = {
matcher: "/api/:path*",
}