feat: SF stays standalone forever; strengthen Python/Rust detection

ADR-019 framing corrections:
- SF is single-machine, single-user, single-repo by design — character, not
  limitation. Stays a standalone app permanently; does not get absorbed into ACE.
- Phase 6 reframed: "pattern transfer" not "orchestration convergence." ACE
  ports patterns from SF, both apps remain independent.
- Phase 2 reframed: SF stays local. Federation is an ACE concern; SF doesn't
  wire memory-store remote-mode against singularity-memory.

Detection strengthened for Python (priority for ace-coder work):
- Detect uv / poetry / pdm and prefix verification commands accordingly
- Emit ruff check when configured (file or [tool.ruff] in pyproject.toml)
- Emit mypy / pyright when configured — skip when no config to avoid false fails
- pyprojectHasTool helper for [tool.<name>] section detection

Detection strengthened for Rust:
- cargo fmt --check (fastest, catches style first)
- cargo check (type-only, faster than test)
- cargo clippy -- -D warnings (warnings as errors)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mikael Hugo 2026-05-01 23:48:17 +02:00
parent 2280893464
commit e7519e904d
2 changed files with 117 additions and 28 deletions

View file

@ -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 35), 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.
---

View file

@ -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.<name>] 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.<name>]` 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);
}