diff --git a/docs/dev/ADR-019-workspace-vm-convergence.md b/docs/dev/ADR-019-workspace-vm-convergence.md index 857bf80e7..74ed854ab 100644 --- a/docs/dev/ADR-019-workspace-vm-convergence.md +++ b/docs/dev/ADR-019-workspace-vm-convergence.md @@ -18,12 +18,15 @@ Two autonomous agent systems are being developed in parallel: - **SF** (`singularity-forge`) — TypeScript orchestrator. Works today. Dispatches Claude Code sessions as ephemeral units (milestone → slice → task). Isolation - via git worktrees. Single-repo, single-user. + via git worktrees. **Single-machine, single-user, single-repo by design.** That + scope is its character, not a limitation. SF stays a standalone app permanently; + it does not grow into a platform. - **ACE** (`ace-coder`) — Python platform. Partially operational. HTDAG execution backbone, Project Manager ownership, 20 defined agent personas, LiteLLM multi-provider, RBAC, PGMQ task queue, tiered memory. Multi-tenant data model - (`tenant_id`) exists; per-task execution isolation does not. + (`tenant_id`) exists; per-task execution isolation does not. ACE is where + multi-tenant, multi-repo, federated workloads live. - **singularity-memory** — Separate Go service (migrating from Python per ADR-014). Postgres + vchord vector store. Federated knowledge layer. @@ -35,15 +38,22 @@ Two autonomous agent systems are being developed in parallel: memory while they help build the system; it is not the production wire for internal services and is expected to shrink once the system is self-hosting. -Both systems share the same end destination but are approaching it from different -directions. SF is production-reliable but architecturally constrained (single-repo, -git-worktree isolation). ACE has the right orchestration primitives (HTDAG, PM, -RBAC, tenant model) but lacks execution isolation and is not yet production-reliable. +The two systems are **not converging into one app.** They occupy different niches: -The strategy is **incremental convergence**: SF continues to work and delivers value -while autonomously helping build out ACE. As ACE becomes reliable, SF's dispatch -model transitions to use ACE's execution substrate. They meet at the workspace VM -boundary. +- SF is the local single-user developer tool — fast, generic, runs on the developer's + machine on whatever repo they're working on. +- ACE is the multi-tenant platform — federated, multi-repo, scales beyond one user. + +Convergence in this ADR refers to **shared substrate**, not application merging: +shared wire schemas (singularity-grpc), shared execution isolation primitive +(Firecracker workspaces) when SF chooses to dispatch into one. SF can live entirely +on its own without ACE; ACE doesn't depend on SF. + +The strategy is **incremental pattern transfer**: SF continues to work as a +standalone single-user tool while autonomously helping build out ACE. ACE ports +proven patterns from SF as it matures. SF gains an optional engine adapter for +dispatching units into ACE workspaces when multi-tenant or multi-repo work is +needed. Neither replaces the other. --- @@ -196,16 +206,20 @@ build system. - ACE develops its HTDAG, PM, and worker primitives independently. - Both systems mature on their own tracks. -### Phase 2 — Federated memory (near-term, ADR-012 Tier 1) -- Wire `memory-store.ts` remote-mode → singularity-memory HTTP endpoint (typed - TS client generated from the Go API — not MCP). -- SF instances on different machines share learnings. -- ACE connects to the same singularity-memory endpoint via a typed Python client - (also generated, also not MCP). Internal services do not pay the MCP tax. +### Phase 2 — Federated memory for ACE (near-term, ADR-012 Tier 1) +- ACE connects to singularity-memory via a typed Python client (generated from + the Go API — not MCP). Internal services do not pay the MCP tax. +- **SF stays local.** SF is single-machine, single-user, local-first by design. + `memory-store.ts` continues to work on `.sf/memory/`; no remote mode wired in + SF core. When SF runs inside an ACE-managed workspace, the workspace surfaces + federated context through the ACE engine adapter as additional KNOWLEDGE + injection — SF doesn't know that's where it came from. Federation is an ACE + concern, not a SF concern. - The MCP façade on singularity-memory is reserved for external coding tools (Claude Code, Cursor) that need to read/write memory while helping build the system. Temporary scaffold; not a production wire. -- **Outcome:** shared knowledge layer operational before execution convergence. +- **Outcome:** federated knowledge layer operational for ACE; SF unchanged and + unaware of memory federation infrastructure. ### Phase 3 — Workspace VM opt-in for SF (medium-term) - Build `sf-workspace` shim: thin Rust binary that manages Firecracker VMs. @@ -227,14 +241,24 @@ build system. - The `sf-workspace` shim and ACE's VM dispatch path are the same binary. - **Outcome:** two orchestrators, one execution substrate. -### Phase 6 — Orchestration convergence (long-term) -- SF's state machine (milestone → slice → task) becomes an ACE workflow spec - (compiled DAG via ACE's `graph_compiler`), not a hand-coded state machine. -- ACE's HTDAG becomes the unified orchestration backbone. -- SF's CLI and headless mode remain as user-facing entry points; they drive ACE - via the existing JSON-RPC stdio contract (already in `packages/rpc-client/`), - not via MCP. MCP at this layer would be redundant — both ends are first-party. -- **Outcome:** one system with SF's reliability and ACE's generality. +### Phase 6 — Pattern transfer (long-term) +**SF remains a separate, standalone app — permanently.** It is not absorbed, +re-platformed, or re-implemented inside ACE. The convergence is at the wire and +execution-substrate layers (Phases 3–5), not at the application layer. + +What Phase 6 actually means: +- ACE ports proven patterns from SF — idempotency primitives, state-derivation + discipline, the structured notification model, the watchdog pattern, project + preferences as a config layer, scaffold-as-contract. These become ACE's own + primitives, written in Python, owned by ACE. +- SF stays single-machine, single-user, local-first — its character. SF gets + *generally* better as a standalone tool: better project detection, cleaner + engine adapter extension point, harder-tested crash recovery. +- SF and ACE remain independent runtimes. SF can be dispatched into an ACE + workspace (Phase 5) for multi-tenant or multi-repo work, but it is also fully + usable on its own with no ACE present. +- **Outcome:** two distinct apps that share wire schemas (singularity-grpc) and + optionally an execution substrate (Firecracker). Neither replaces the other. --- diff --git a/src/resources/extensions/sf/detection.ts b/src/resources/extensions/sf/detection.ts index be82c5ec9..ced4b65dc 100644 --- a/src/resources/extensions/sf/detection.ts +++ b/src/resources/extensions/sf/detection.ts @@ -774,9 +774,13 @@ function detectVerificationCommands( } if (detectedFiles.includes("Cargo.toml")) { + // Format check first — fastest, catches style drift before anything else runs. + commands.push("cargo fmt --check"); + // Type-check without running tests (faster than test, catches most regressions). + commands.push("cargo check"); // Limit test threads so Rust tests don't saturate all CPUs. commands.push("cargo test -- --test-threads=2"); - commands.push("cargo clippy"); + commands.push("cargo clippy -- -D warnings"); } if (detectedFiles.includes("go.mod")) { @@ -812,8 +816,48 @@ function detectVerificationCommands( detectedFiles.includes("setup.py") || detectedFiles.includes("requirements.txt") ) { - // Single-process pytest by default; -x stops on first failure (fast feedback). - commands.push("pytest -x"); + // Detect Python package manager. uv > poetry > pdm > raw. + // The runner prefix changes which python gets invoked, so it matters that + // commands match the project's actual env. + const hasUvLock = existsSync(join(basePath, "uv.lock")); + const hasPoetryLock = existsSync(join(basePath, "poetry.lock")); + const hasPdmLock = existsSync(join(basePath, "pdm.lock")); + const pyRunner = hasUvLock + ? "uv run" + : hasPoetryLock + ? "poetry run" + : hasPdmLock + ? "pdm run" + : ""; + const prefix = pyRunner ? `${pyRunner} ` : ""; + + // Lint first — ruff is fast and catches drift before slower checks run. + const hasRuff = + existsSync(join(basePath, "ruff.toml")) || + existsSync(join(basePath, ".ruff.toml")) || + pyprojectHasTool(basePath, "ruff"); + if (hasRuff) { + commands.push(`${prefix}ruff check`); + } + + // Type check — only emit if config exists (mypy or pyright). + // Without config these tools error confusingly on first run; better to + // skip than to emit a command that always fails. + const hasMypy = + existsSync(join(basePath, "mypy.ini")) || + existsSync(join(basePath, ".mypy.ini")) || + pyprojectHasTool(basePath, "mypy"); + const hasPyright = + existsSync(join(basePath, "pyrightconfig.json")) || + pyprojectHasTool(basePath, "pyright"); + if (hasMypy) { + commands.push(`${prefix}mypy .`); + } else if (hasPyright) { + commands.push(`${prefix}pyright`); + } + + // Tests — single-process pytest by default; -x stops on first failure. + commands.push(`${prefix}pytest -x`); } if (detectedFiles.includes("Gemfile")) { @@ -913,6 +957,27 @@ function readMakefileTargets(basePath: string): string[] { } } +/** + * Detect whether a Python tool is configured under [tool.] in pyproject.toml. + * Used by Python verification command detection so we only emit `mypy` / `pyright` / + * `ruff` invocations for projects that actually configure those tools. + * + * Naive substring scan — avoids pulling in a TOML parser for a check this simple. + * Matches the standard `[tool.]` section header at the start of a line. + */ +function pyprojectHasTool(basePath: string, toolName: string): boolean { + try { + const raw = readFileSync(join(basePath, "pyproject.toml"), "utf-8"); + const header = `[tool.${toolName}]`; + for (const line of raw.split("\n")) { + if (line.trim().startsWith(header)) return true; + } + return false; + } catch { + return false; + } +} + function pushUnique(arr: string[], value: string): void { if (!arr.includes(value)) arr.push(value); }