Skip to content

Scan Command

Overview

The Scan Command discovers projects in a workspace and syncs their metadata into the Symphony database. It runs file system detection to identify projects by type (backend, frontend, fullstack, module), upserts project records with slug/path matching, and seeds agent profiles for new projects. Scan is invoked manually (symphony scan) or automatically during startup to keep the project registry current.

How It Works

Detection Strategy

Scan uses two-tier detection to find projects:

  1. Parent Directory Check — If the parent directory itself contains a project marker and .git, treat it as the single project. This handles Symphony inside a monorepo root.

  2. Subdirectory Scan — Otherwise, scan immediate subdirectories for project markers:

    • angular.json → frontend
    • nuxt.config.{ts,js} → fullstack
    • next.config.{js,mjs,ts} → fullstack
    • composer.json → backend
    • Cargo.toml, go.mod, pyproject.toml, requirements.txt → backend
    • package.json → frontend (refined below)
  3. Type Refinement — For package.json projects, refine the type by inspecting dependencies:

    • If both server framework (express, fastify, nitro, h3, etc.) and client framework (react, vue, svelte, angular, etc.) detected → fullstack
    • If server framework detected → backend
    • If server directory exists (server/api, server/routes, src/server, src/api) → fullstack
    • Otherwise → frontend
  4. Self-Detection Fallback — If no sibling projects found and selfDirName is provided, detect the Symphony directory itself as a project. This handles standalone Symphony installations.

  5. Generic Fallback — If nothing is detected, register the parent directory as a generic module type.

Sync Strategy

Once projects are detected, sync matches them to existing DB records using a two-key lookup:

Match existing projects:

  • By slug (derived from project name via slugify)
  • By path (project filesystem location)

Update scenarios:

  • Path changed → project moved (update path)
  • Name changed → re-detected with correct name (update name + slug)
  • Type/detectedFrom changed → update fields

Insert new projects:

  • Generate unique identifier prefix
  • Create project record with slug, type, path
  • Seed default agent profiles (Planner, Worker, Judge, Researcher, Architect)

Backfill profiles:

  • Ensure existing projects have profiles (catches projects created before profile seeding)

Data Model

DetectedProject:

  • name — basename of directory
  • path — absolute filesystem path
  • type — 'backend' | 'frontend' | 'fullstack' | 'module'
  • detectedFrom — filename of marker that triggered detection

Project (DB):

  • id — UUID
  • name — display name
  • slug — URL-safe identifier
  • identifierPrefix — issue prefix (e.g., "SYM-")
  • path — absolute path
  • type — detected type
  • detectedFrom — marker file
  • config — JSON config (gates, risk settings, profile overrides)
  • issueCounter — next issue number

Key Components

FileResponsibility
cli/commands/scan.tsEntry point; delegates to detector and sync; formats output
server/services/projectDetector.tsTwo-tier detection (parent + subdirs), type refinement, self-detection fallback
server/services/projectSync.tsDB upsert logic; slug/path matching; profile seeding and backfill
cli/utils/initCli.tsCLI initialization; loads config, opens DB, runs migrations
cli/index.tsCommand routing; orchestrates CLI flow

Design Decisions

1. Two-Tier Detection (Parent + Subdirectories)

Why: Handles both monorepos (where Symphony is in a root dir with multiple projects) and workspaces with separate projects. Trade-off: Adds complexity but covers all common layouts. Alternative: only scan subdirectories (misses monorepo case).

2. Type Refinement via Dependency Inspection

Why: package.json alone doesn't distinguish frontend from backend/fullstack. Server frameworks and directory structure provide reliable signals. Trade-off: Requires parsing package.json, but more accurate than marker-only detection. Cost is low (single file read per package.json project).

3. Self-Detection Fallback

Why: When no sibling projects are found, Symphony itself might be the only project (standalone installation). Self-detection enables use in single-project setups. Trade-off: Adds one more detection attempt, but fallback is essential for standalone use.

4. Generic Module Fallback

Why: If all detection strategies fail, still register a project (prevents "no projects found" errors). Trade-off: May register unexpected directories as projects, but better than silent failure. Users can manage bad registrations via UI.

5. Slug-Based Matching with Path Fallback

Why: Slug is stable across renames (name → slug is deterministic). Path fallback catches moved projects. Together they handle most project lifecycle events. Trade-off: False positives if two different projects have the same slug (unlikely with slugify). No collision detection; last-upsert-wins.

6. Profile Seeding on Insert + Backfill

Why: New projects need default profiles for dispatch to work. Backfill catches projects created before this logic existed. Trade-off: Two queries per sync (check if profiles exist, seed if missing). Cost is negligible compared to detection overhead.

7. Skip Common Build/Dep Directories

Why: node_modules, .git, dist, build, .nuxt, .output are not projects and cause slow scans. Skip them to reduce I/O. Trade-off: Users can't have projects named these names. Acceptable constraint.

8. Deterministic Identifier Prefix Generation

Why: Identifier prefix (e.g., "SYM-") is derived from project name to keep issue numbers stable and human-readable. Trade-off: Collision possibility if multiple projects resolve to the same prefix. generatePrefix handles by appending suffix.

Known Gaps

Symlinked directories are not explicitly checked. statSync will follow symlinks, but circular symlinks may cause hangs. No timeout on readdirSync.

2. Performance on Large Workspaces

Scanning hundreds of directories triggers many statSync calls. No parallelization. Acceptable for typical workspaces (<50 projects), but may be slow for monorepos.

3. Slug Collision Detection

If two projects slugify to the same value, sync will update one and ignore the other (last-upsert-wins). No warning or error.

4. Marker File Ordering

MARKERS array is ordered, so angular.json takes precedence over package.json. If a directory has both, it's detected as frontend (Angular) not fullstack. Unlikely but possible.

5. Type Refinement Accuracy

Presence of dependencies is assumed to indicate project type, but a fullstack project might not list client or server frameworks explicitly. Fallback on existence of server directories helps but isn't foolproof.

6. Self-Detection Name Inference

When Symphony is the only detected project and no configProjectName override is provided, the project is named after the Symphony directory (e.g., "symphony" or "ralph-loop"). Unintuitive in some contexts.

7. No Deletion Logic

If a project is removed from the filesystem but exists in the DB, it remains registered. No cleanup or archival. Manual deletion required via UI.

8. Config Name Override Scoping

configProjectName only applies when Symphony detects itself as the sole project. Multi-project workspaces cannot override names via config.