Environment API

The Environment API scans project directories to discover tools, frameworks, and configuration artifacts. It powers profile suggestions, checkpoint/rollback safety, config file synchronization, and template capture for repeatable setups.

Quick Start

Scan a project directory, review discovered artifacts, and sync a config file — a typical onboarding flow:

// 1. Trigger an environment scan on a project directory
interface ScanSummary {
totalArtifacts: number;
personas: string[];
durationMs: number;
}

const { scan, summary }: { scan: { id: string }; summary: ScanSummary } =
await fetch('/api/environment/scan', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    projectDir: '/Users/team/my-app',
    projectId: 'proj-8f3a-4b2c',
  }),
}).then((r: Response) => r.json());

console.log(`Found ${summary.totalArtifacts} artifacts in ${summary.durationMs}ms`);
console.log(`Personas: ${summary.personas.join(', ')}`);

// 2. Browse discovered artifacts (e.g. config files, dependencies)
const { artifacts }: { artifacts: { name: string; tool: string }[] } =
await fetch('/api/environment/artifacts?category=config').then((r: Response) => r.json());
artifacts.forEach((a) => console.log(`  ${a.name} (${a.tool})`));

// 3. Sync a configuration file (with automatic checkpoint for safety)
const syncResult: { checkpointId: string; summary: { applied: number; failed: number } } =
await fetch('/api/environment/sync', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    operations: [{ targetPath: '.prettierrc', content: '{ "semi": false, "singleQuote": true }' }],
    label: 'Apply prettier config',
  }),
}).then((r: Response) => r.json());

// 4. Get profile suggestions based on the scan
const { curated, discovered }: { curated: unknown[]; discovered: unknown[] } =
await fetch('/api/environment/profiles/suggest?tiered=true').then((r: Response) => r.json());
console.log(`${curated.length} curated + ${discovered.length} discovered suggestions`);

Base URL

/api/environment

Scanning

Trigger Environment Scan

POST /api/environment/scan

Run a new environment scan against a project directory. Discovers artifacts, tools, and framework personas.

Request Body

FieldTypeReqDescription
projectDirstringDirectory to scan (defaults to launch CWD)
projectIdstringProject ID to associate the scan with

Response Body

FieldTypeReqDescription
scanobject*Scan record with ID, timestamps, and metadata
summary.totalArtifactsnumber*Total artifacts discovered
summary.personasstring[]*Detected tool personas (e.g., node, python, rust)
summary.categoryCountsobject*Artifact counts by category
summary.toolCountsobject*Artifact counts by tool/framework
summary.durationMsnumber*Scan duration in milliseconds
summary.errorsnumber*Number of scan errors

Scan a project to discover its toolchain and generate profile suggestions:

// Scan a project directory to discover its environment
interface ScanResult {
scan: { id: string };
summary: {
  totalArtifacts: number;
  personas: string[];
  categoryCounts: Record<string, number>;
  durationMs: number;
};
}

const { scan, summary }: ScanResult = await fetch('/api/environment/scan', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  projectDir: '/Users/team/my-app',
  projectId: 'proj-8f3a-4b2c',
}),
}).then((r: Response) => r.json());

console.log(`Scan ${scan.id}: ${summary.totalArtifacts} artifacts in ${summary.durationMs}ms`);
console.log(`Personas: ${summary.personas.join(', ')}`);
console.log(`Categories: ${JSON.stringify(summary.categoryCounts)}`);

Example response:

{
  "scan": {
    "id": "scan-1a2b-3c4d",
    "projectDir": "/Users/team/my-app",
    "projectId": "proj-8f3a-4b2c",
    "createdAt": "2026-04-03T10:00:00.000Z"
  },
  "summary": {
    "totalArtifacts": 24,
    "personas": ["node", "typescript", "react"],
    "categoryCounts": { "config": 12, "dependency": 8, "build": 4 },
    "toolCounts": { "node": 14, "typescript": 6, "eslint": 4 },
    "durationMs": 342,
    "errors": 0
  }
}

Get Latest Scan

GET /api/environment/scan

Retrieve the latest scan result. Auto-triggers a fresh scan if projectDir is provided and the cached scan is stale.

Query Parameters

Param Type Req Description
projectId string Filter by project ID
scanId string Fetch a specific scan by ID
projectDir string Project directory (triggers auto-scan if stale)

Response Body

FieldTypeReqDescription
scanobject | null*Scan record or null if none found
summary.categoryCountsobjectArtifact counts by category
summary.toolCountsobjectArtifact counts by tool/framework

Fetch the latest scan for a project — or pass projectDir to auto-trigger a fresh scan if stale:

// Get the latest scan, auto-refresh if stale
const { scan, summary }: {
scan: { createdAt: string } | null;
summary: { categoryCounts: Record<string, number> };
} = await fetch('/api/environment/scan?projectId=proj-8f3a-4b2c')
.then((r: Response) => r.json());

if (scan) {
console.log(`Last scanned: ${scan.createdAt}`);
console.log(`Artifacts: ${JSON.stringify(summary.categoryCounts)}`);
} else {
console.log('No scan found — trigger one with POST /api/environment/scan');
}

Artifacts

List Artifacts

GET /api/environment/artifacts

Filtered artifact list from the latest (or specified) scan.

Query Parameters

Param Type Req Description
scanId string Scan ID (defaults to latest)
category string Filter by artifact category
tool string Filter by tool persona
scope string Filter by artifact scope
search string Free-text search across artifact names

Response Body

FieldTypeReqDescription
artifactsobject[]*Array of artifact objects
countnumber*Total matching artifacts

Browse discovered config files for a specific tool — useful for reviewing what the scanner found:

// List Node.js config artifacts
interface Artifact {
name: string;
filePath: string;
}

const { artifacts, count }: { artifacts: Artifact[]; count: number } =
await fetch('/api/environment/artifacts?category=config&tool=node')
  .then((r: Response) => r.json());

console.log(`${count} Node.js config artifacts:`);
artifacts.forEach((a: Artifact) => console.log(`  ${a.name} — ${a.filePath}`));

Example response:

{
  "artifacts": [
    {
      "id": "art-5e6f-7g8h",
      "scanId": "scan-1a2b-3c4d",
      "name": "package.json",
      "category": "config",
      "tool": "node",
      "scope": "project",
      "filePath": "/Users/team/my-app/package.json"
    },
    {
      "id": "art-9i0j-1k2l",
      "scanId": "scan-1a2b-3c4d",
      "name": ".nvmrc",
      "category": "config",
      "tool": "node",
      "scope": "project",
      "filePath": "/Users/team/my-app/.nvmrc"
    }
  ],
  "count": 2
}

Get Artifact

GET /api/environment/artifacts/{id}

Retrieve a single artifact with full detail.

Response 200{ "artifact": <object> }

const { artifact }: { artifact: { name: string; tool: string; filePath: string } } =
await fetch('/api/environment/artifacts/art-5e6f-7g8h').then((r: Response) => r.json());
console.log(`${artifact.name} (${artifact.tool}) at ${artifact.filePath}`);

Errors: 404 — Artifact not found

Checkpoints

List Checkpoints

GET /api/environment/checkpoints

List all checkpoints, optionally filtered by project.

Query Parameters

Param Type Req Description
projectId string Filter checkpoints by project ID

Response Body

FieldTypeReqDescription
checkpointsobject[]*Array of checkpoint records
// List checkpoints for a project
interface Checkpoint {
label: string;
status: string;
filesCount: number;
createdAt: string;
}

const { checkpoints }: { checkpoints: Checkpoint[] } =
await fetch('/api/environment/checkpoints?projectId=proj-8f3a-4b2c')
  .then((r: Response) => r.json());

checkpoints.forEach((cp: Checkpoint) => {
console.log(`${cp.label} [${cp.status}] — ${cp.filesCount} files — ${cp.createdAt}`);
});

Create Checkpoint

POST /api/environment/checkpoints

Create a new checkpoint with git tag and/or file backup. Git repos get a tagged commit; global config files are backed up to a timestamped directory.

Request Body

FieldTypeReqDescription
labelstringCheckpoint label (auto-generated if omitted)
checkpointTypeenumpre-sync, manual, or pre-onboard (default: manual)
projectDirstringProject directory for git checkpoint
globalPathsstring[]Specific file paths to back up (defaults to common config files)

Response Body

FieldTypeReqDescription
checkpoint.idstring*Checkpoint identifier
checkpoint.labelstring*Checkpoint label
checkpoint.gitTagstringGit tag name if applicable
checkpoint.gitCommitShastringGit commit SHA
checkpoint.backupPathstringPath to file backup directory
checkpoint.filesCountnumber*Number of files captured

Create a checkpoint before making risky changes — captures git state and config files for rollback:

// Create a manual checkpoint before risky changes
interface CheckpointResponse {
checkpoint: {
  id: string;
  filesCount: number;
  gitTag?: string;
  gitCommitSha?: string;
};
}

const { checkpoint }: CheckpointResponse = await fetch('/api/environment/checkpoints', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  label: 'Before dependency upgrade',
  projectDir: '/Users/team/my-app',
}),
}).then((r: Response) => r.json());

console.log(`Checkpoint ${checkpoint.id}: ${checkpoint.filesCount} files captured`);
if (checkpoint.gitTag) {
console.log(`Git tag: ${checkpoint.gitTag} (${checkpoint.gitCommitSha})`);
}

Example response:

{
  "checkpoint": {
    "id": "cp-4d5e-6f7g",
    "label": "Before dependency upgrade",
    "checkpointType": "manual",
    "status": "active",
    "gitTag": "stagent-cp-4d5e-6f7g",
    "gitCommitSha": "a1b2c3d4e5f6",
    "backupPath": "/Users/team/.stagent/checkpoints/cp-4d5e-6f7g",
    "filesCount": 8,
    "createdAt": "2026-04-03T10:00:00.000Z"
  }
}

Get Checkpoint Detail

GET /api/environment/checkpoints/{id}

Get checkpoint details including sync operations and git diff from the checkpoint commit to current HEAD.

Response Body

FieldTypeReqDescription
checkpointobject*Checkpoint record
syncOpsobject[]*Sync operations associated with this checkpoint
diffstring | null*Git diff since checkpoint (null if no git data)

Review what changed since a checkpoint — the diff shows all git changes since the checkpoint commit:

// Review changes since checkpoint
const { checkpoint, syncOps, diff }: {
checkpoint: { label: string; status: string };
syncOps: unknown[];
diff: string | null;
} = await fetch('/api/environment/checkpoints/cp-4d5e-6f7g')
.then((r: Response) => r.json());

console.log(`${checkpoint.label} [${checkpoint.status}]`);
console.log(`${syncOps.length} sync operations`);
if (diff) {
console.log(`Git changes since checkpoint:\n${diff}`);
}

Errors: 404 — Checkpoint not found

Rollback to Checkpoint

POST /api/environment/checkpoints/{id}

Rollback to a checkpoint. Restores git state and file backups. Only works on checkpoints with status 'active'.

Response Body

FieldTypeReqDescription
messagestring*Rollback confirmation message
results.gitobjectGit rollback result (success, error)
results.backupobjectFile restore result (restored count, errors)

Rollback to a checkpoint when changes went wrong — restores both git state and backed-up files:

// Rollback to a checkpoint
interface RollbackResult {
message: string;
results: {
  git?: { success: boolean; error?: string };
  backup?: { restored: number };
};
}

const result: RollbackResult = await fetch('/api/environment/checkpoints/cp-4d5e-6f7g', {
method: 'POST',
}).then((r: Response) => r.json());

console.log(result.message);
if (result.results.git) {
console.log(`Git rollback: ${result.results.git.success ? 'OK' : result.results.git.error}`);
}
if (result.results.backup) {
console.log(`Restored ${result.results.backup.restored} files`);
}

Errors: 400 — Checkpoint not in active status, 404 — Not found, 500 — Git rollback failure

Profile Suggestions

Suggest Profiles

GET /api/environment/profiles/suggest

Suggest agent profiles based on the latest environment scan. Supports flat or tiered (curated vs. discovered) response formats.

Query Parameters

Param Type Req Description
scanId string Scan ID (defaults to latest)
tiered string When "true", return curated and discovered tiers separately

Response Body (flat)

FieldTypeReqDescription
suggestionsobject[]*Array of profile suggestions
countnumber*Total suggestions

Response Body (tiered)

FieldTypeReqDescription
curatedobject[]*High-confidence profile suggestions
discoveredobject[]*Lower-confidence discovered suggestions
curatedCountnumber*Number of curated suggestions
discoveredCountnumber*Number of discovered suggestions

Get tiered profile suggestions — curated suggestions are high confidence based on detected toolchains:

// Get tiered profile suggestions
interface Suggestion {
name: string;
description: string;
}

const { curated, discovered }: { curated: Suggestion[]; discovered: Suggestion[] } =
await fetch('/api/environment/profiles/suggest?tiered=true')
  .then((r: Response) => r.json());

console.log('Recommended profiles:');
curated.forEach((s: Suggestion) => console.log(`  [curated] ${s.name} — ${s.description}`));
discovered.forEach((s: Suggestion) => console.log(`  [discovered] ${s.name} — ${s.description}`));

Example response:

{
  "curated": [
    {
      "ruleId": "node-fullstack",
      "name": "Node.js Fullstack",
      "description": "Full-stack Node.js development with TypeScript and React",
      "confidence": 0.95,
      "matchedArtifacts": ["package.json", "tsconfig.json", "next.config.js"]
    }
  ],
  "discovered": [
    {
      "ruleId": "docker-ops",
      "name": "Docker Ops",
      "description": "Container management and Dockerfile optimization",
      "confidence": 0.6,
      "matchedArtifacts": ["Dockerfile"]
    }
  ],
  "curatedCount": 1,
  "discoveredCount": 1
}

Create Profile from Suggestion

POST /api/environment/profiles/create

Create an agent profile from a scan-based suggestion with optional overrides.

Request Body

FieldTypeReqDescription
suggestionobject*Profile suggestion object from /profiles/suggest
overrides.namestringOverride the suggested profile name
overrides.descriptionstringOverride the suggested description
overrides.systemPromptstringOverride the suggested system prompt

Response 201{ "message": "Profile created", "profileId": "env-<ruleId>" }

Create a profile from a suggestion — optionally override the name or system prompt:

// Create a profile from the top curated suggestion
const profile: { message: string; profileId: string } = await fetch('/api/environment/profiles/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  suggestion: curated[0],
  overrides: { name: 'My Node Profile' },
}),
}).then((r: Response) => r.json());

console.log(`Profile created: ${profile.profileId}`);

Errors: 400 — Missing suggestion or creation failure

Config Sync

Preview Sync

POST /api/environment/sync/preview

Dry-run sync operations to see what would change without modifying any files.

Request Body

FieldTypeReqDescription
operationsSyncRequest[]*Array of sync operations to preview

Response Body

FieldTypeReqDescription
previewsobject[]*Preview results with diffs and conflict flags
summary.totalnumber*Total operations
summary.conflictsnumber*Operations with conflicts
summary.newFilesnumber*New files that would be created
summary.totalAdditionsnumber*Total lines added
summary.totalDeletionsnumber*Total lines removed

Preview a sync before executing — check for conflicts and see what would change:

// Preview what would change before applying
interface PreviewResult {
previews: unknown[];
summary: {
  total: number;
  conflicts: number;
  totalAdditions: number;
  totalDeletions: number;
};
}

const preview: PreviewResult = await fetch('/api/environment/sync/preview', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  operations: [
    { targetPath: '.prettierrc', content: '{ "semi": false, "singleQuote": true }' },
  ],
}),
}).then((r: Response) => r.json());

console.log(`${preview.summary.total} operations, ${preview.summary.conflicts} conflicts`);
console.log(`+${preview.summary.totalAdditions} / -${preview.summary.totalDeletions} lines`);

Example response:

{
  "previews": [
    {
      "targetPath": ".prettierrc",
      "status": "new_file",
      "hasConflict": false,
      "diff": "+{ \"semi\": false, \"singleQuote\": true }"
    }
  ],
  "summary": {
    "total": 1,
    "conflicts": 0,
    "newFiles": 1,
    "totalAdditions": 1,
    "totalDeletions": 0
  }
}

Execute Sync

POST /api/environment/sync

Execute sync operations with an automatic pre-sync checkpoint for safety. Previews all operations first, then applies changes.

Request Body

FieldTypeReqDescription
operationsSyncRequest[]*Array of sync operations to execute
labelstringLabel for the auto-created checkpoint

Response Body

FieldTypeReqDescription
checkpointIdstring*ID of the auto-created pre-sync checkpoint
operationsobject[]*Operation results with status per operation
summary.appliednumber*Successfully applied operations
summary.failednumber*Failed operations
summary.totalnumber*Total operations attempted

Execute sync with a labeled checkpoint — the checkpoint is created automatically before any files are modified:

// Sync config files (automatic checkpoint created for safety)
interface SyncResult {
checkpointId: string;
summary: { applied: number; failed: number };
}

const result: SyncResult = await fetch('/api/environment/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  operations: [
    { targetPath: '.prettierrc', content: '{ "semi": false, "singleQuote": true }' },
  ],
  label: 'Apply prettier config',
}),
}).then((r: Response) => r.json());

console.log(`Checkpoint: ${result.checkpointId}`);
console.log(`Applied: ${result.summary.applied}, Failed: ${result.summary.failed}`);

Errors: 400 — No operations provided or no valid operations

Sync History

GET /api/environment/sync/history

List past sync operations grouped by their pre-sync checkpoints.

Query Parameters

Param Type Req Description
projectId string Filter by project ID
limit number Max checkpoints to return (default: 20)

Response Body

FieldTypeReqDescription
historyobject[]*Array of { checkpoint, operations } objects
countnumber*Number of history entries

Review past sync operations to see what was changed and when:

// Review recent sync history
interface SyncHistoryEntry {
checkpoint: { label: string; createdAt: string };
operations: unknown[];
}

const { history }: { history: SyncHistoryEntry[] } =
await fetch('/api/environment/sync/history?limit=5').then((r: Response) => r.json());

history.forEach((entry: SyncHistoryEntry) => {
console.log(`${entry.checkpoint.label} — ${entry.operations.length} ops — ${entry.checkpoint.createdAt}`);
});

Templates

List Templates

GET /api/environment/templates

List all captured environment templates.

Response Body

FieldTypeReqDescription
templatesobject[]*Array of template records
// List available environment templates
interface Template {
name: string;
description: string;
}

const { templates }: { templates: Template[] } =
await fetch('/api/environment/templates').then((r: Response) => r.json());
templates.forEach((t: Template) => console.log(`${t.name} — ${t.description}`));

Capture Template

POST /api/environment/templates

Capture a new template from an existing scan, preserving the environment configuration for reuse.

Request Body

FieldTypeReqDescription
scanIdstring*Scan ID to capture from
namestring*Template name
descriptionstringTemplate description

Response 201{ "template": <object> }

Capture a well-configured project environment as a reusable template:

// Capture the current environment as a template
const { template }: { template: { id: string } } = await fetch('/api/environment/templates', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  scanId: 'scan-1a2b-3c4d',
  name: 'Next.js Starter',
  description: 'Next.js 15 + TypeScript + Tailwind baseline configuration',
}),
}).then((r: Response) => r.json());

console.log(`Template saved: ${template.id}`);

Errors: 400 — Missing scanId or name

Get Template

GET /api/environment/templates/{id}

Get template detail with parsed manifest showing all captured configuration.

Response Body

FieldTypeReqDescription
templateobject*Template record
manifestobject*Parsed manifest with artifacts and config
const { template, manifest }: {
template: { name: string };
manifest: { artifacts: unknown[] };
} = await fetch('/api/environment/templates/tpl-1a2b-3c4d')
.then((r: Response) => r.json());
console.log(`${template.name}: ${manifest.artifacts.length} artifacts`);

Errors: 404 — Template not found

Delete Template

DELETE /api/environment/templates/{id}

Permanently delete a template.

Response 200{ "message": "Template deleted" }

await fetch('/api/environment/templates/tpl-1a2b-3c4d', { method: 'DELETE' });

Errors: 404 — Template not found