Skip to content

Development Workflow

Sprawl uses tsx for TypeScript execution, Vitest for testing, and Just for task orchestration. All TS apps run directly from source — no build step. Optic is the exception (Rust, compiled with cargo).

FileRole
JustfileTask runner (primary interface)
pnpm-workspace.yamlWorkspace config
apps/*/package.jsonApp dependencies
packages/*/package.jsonPackage dependencies
CommandDescription
just devConstruct dev mode (file watching)
just start <instance>Start named Construct instance (reads .env.<instance>)
just cli [instance] [args]Construct CLI
just cortex-devCortex dev mode
just cortex-startCortex production
just cortex-backfill [days]Backfill historical data
just synapse-devSynapse dev mode
just synapse-startSynapse production
just synapse-statusPortfolio summary
just deck-dev <instance>Deck dev mode
just optic [db] [synapse]Optic TUI
just optic-buildBuild Optic release binary
just testRun all tests (pnpm -r run test)
just test-constructConstruct tests only
just test-cairnCairn tests only
just test-synapseSynapse tests only
just test-aiAI integration tests
just typecheckTypecheck all packages
just db-migrate [inst]Run Construct DB migrations

Each app reads its env from .env.construct, .env.cortex, .env.synapse, .env.loom, etc. Named instances (e.g., just start myinstance) read from .env.myinstance.

{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}

Key points:

  • No compilation: noEmit: true — tsx handles runtime transpilation
  • Path alias: @/* maps to ./src/* (used in vitest config)
  • Strict mode: Full TypeScript strict checks enabled
  • Bundler module resolution: Modern resolution compatible with tsx

The project uses tsx (via --import=tsx) as a TypeScript loader. This means:

  • No build step required
  • Source files are transpiled on-the-fly
  • File watching uses Node.js native --watch-path flag
  • Extension tool files use jiti instead of tsx for dynamic loading
export default defineConfig({
resolve: {
alias: { '@': new URL('./src', import.meta.url).pathname },
},
test: {
globals: true,
environment: 'node',
},
})
  • Global test functions: describe, it, expect, etc. are available without imports
  • Node environment: Tests run in Node.js (not jsdom)
  • Path alias: @/ resolves to src/ in test files

Tests are colocated with their source in __tests__/ directories:

src/tools/core/__tests__/
memory.test.ts
schedule.test.ts
src/tools/self/__tests__/
deploy.test.ts
exec.test.ts
extension-scope.test.ts
self.test.ts
src/tools/web/__tests__/
web.test.ts
src/tools/__tests__/
packs.test.ts
src/extensions/__tests__/
dynamic-tools.test.ts
loader.test.ts
secrets.test.ts
skills.test.ts
Terminal window
just test # Run all tests (pnpm -r run test)
just test-construct # Construct tests only
just test-cairn # Cairn tests only
npx vitest run -t memory # Filter by test name

The self_run_tests tool also runs npx vitest run --reporter=verbose with a 60-second timeout.

The logging system uses @logtape/logtape with these loggers:

LoggerCategory
log['construct']
agentLog['construct', 'agent']
toolLog['construct', 'tool']
telegramLog['construct', 'telegram']
schedulerLog['construct', 'scheduler']
dbLog['construct', 'db']
  • Console: Always active, uses a custom formatter
  • File: Active when LOG_FILE is set. Uses a swappable WriteStream to support runtime log rotation.
2026-02-24T15:30:00.000Z [info] construct.agent: Processing message from telegram
  • Automatic: On startup, if the log file exceeds 5 MB, it is rotated
  • Manual: The self_system_status tool can trigger rotation via rotate_logs: true
  • Rotation keeps up to 3 archived files: construct.log.1, construct.log.2, construct.log.3

The self_deploy tool handles automated deployment. It detects the runtime environment by checking for /.dockerenv:

Common steps (both environments):

  1. Typecheck (tsc --noEmit)
  2. Test (vitest run)
  3. Git tag backup (pre-deploy-TIMESTAMP)
  4. Git commit (src/, cli/, and extensions/ directories)

Docker mode (detected via /.dockerenv):

  1. process.exit(0) — container restarts via restart: unless-stopped policy

Systemd mode (non-Docker):

  1. sudo systemctl restart construct
  2. Health check (5-second wait, then systemctl is-active)
  3. Auto-rollback on failure (git revert HEAD, restart)

Self-deploy is:

  • Disabled in development mode (NODE_ENV=development)
  • Rate-limited to 3 deploys per hour
  • Safety-gated by a confirm: true parameter

See Deployment Guide for full details on Docker and systemd deployment, and Security Considerations for the complete safety model.

When NODE_ENV=development:

  • File watching is active (--watch-path)
  • self_deploy tool is not loaded (returns null from factory)
  • Context preamble includes [DEV MODE] and a development warning
  • EXTENSIONS_DIR defaults to ./data instead of XDG path
PackageVersionPurpose
@mariozechner/pi-agent-core^0.54.2Agent framework
@mariozechner/pi-ai^0.54.2LLM model access
@sinclair/typebox^0.34.48JSON Schema / TypeBox for tool parameters
grammy^1.40.0Telegram Bot API
kysely^0.28.11Type-safe SQL query builder
croner^10.0.1Cron job scheduling
citty^0.2.1CLI framework
jiti^2.6.1Dynamic TypeScript loading (extensions)
@logtape/logtape^2.0.2Structured logging
nanoid^5.1.6ID generation
yaml^2.8.2YAML parsing (skill frontmatter)
zod^4.3.6Environment validation
date-fns^4.1.0Date utilities
chalk^5.6.2Terminal coloring
consola^3.4.2Console utilities
PackageVersionPurpose
typescript^5.9.3Type checking
tsx^4.21.0TypeScript execution
vitest^4.0.18Testing framework
@types/node^25.3.0Node.js type definitions