Skip to content

Projects

jstuart0 edited this page Apr 27, 2026 · 1 revision

Projects

Projects are first-class records that group sessions and templates by working directory. They were introduced in v0.2.0-pre.3.

Why they exist

Before projects, every session was a flat row. Two sessions running in /Users/me/dev/agentpulse and one running in /Users/me/dev/myapp looked identical on the dashboard apart from the cwd column. Templates carried a cwd field but no concept of "this template belongs to a project, so use the project's defaults for agentType / model / launchMode."

Projects fix both:

  • Sessions auto-resolve to the right project on event ingest by longest-prefix cwd match.
  • Templates inherit project defaults with per-field overrides — change the project's defaultAgentType and every linked template renders the new value on next read.

Data model

projects
  id                          TEXT PK
  name                        TEXT UNIQUE
  cwd                         TEXT NOT NULL  ← normalized, no trailing slash
  github_repo_url             TEXT (optional)
  default_agent_type          TEXT (claude_code | codex_cli | null)
  default_model               TEXT (provider-specific, optional)
  default_launch_mode         TEXT (interactive_terminal | headless | managed_codex | null)
  notes                       TEXT
  tags                        JSON
  is_favorite                 INTEGER
  created_at, updated_at      TEXT

sessions.project_id           TEXT (nullable FK)  ← stamped on event ingest
session_templates.project_id  TEXT (nullable FK)
session_templates.template_project_overrides
                              JSON array of field names the user explicitly set
project_alert_rules           per-project alert rule rows (see below)

session_templates.project_id is nullable because some templates intentionally don't bind to a project. template_project_overrides is a JSON array of field names the user explicitly set; absent fields inherit from the project.

Cwd resolution

The resolver lives at src/server/services/projects/resolver.ts. It's pure (no DB calls), takes the session's cwd and the cached project list, and returns the longest-prefix match.

Path-segment-aware: /foo/bar matches a project at /foo/bar or /foo/bar/sub but not /foo/barbaz. The cache is loaded eagerly at server boot before the hook ingestion route is mounted, so the first event after restart never blocks on a DB read.

A one-shot backfill on boot stamps project_id on pre-existing sessions whose cwd matches an existing project. Re-resolution also runs on createProject / updateProject (when cwd changes) so all matching sessions update in one transaction.

Auto-create on template save

Saving a template without an explicit projectId triggers ensureProjectForCwd:

  1. Look up an existing project whose normalized cwd equals the template's cwd. If found, link the template to it.
  2. Otherwise create a new project with the cwd, derive the name from the cwd basename (with -2 / -3 numeric suffix on collision), and link the template to it.

The dropdown in the template editor reads "Auto (match by directory)" as its default option to communicate this. The user can still pick an explicit project from the list, or create one separately at /projects first.

Template ↔ project inheritance

Linking a template to a project enables live inheritance for these fields:

Field Inherited from project? Override sentinel
cwd ✓ project.cwd template_project_overrides includes cwd
agentType ✓ project.defaultAgentType (when non-null) template_project_overrides includes agentType
model ✓ project.defaultModel (when non-null) template_project_overrides includes model
name, description, taskPrompt, baseInstructions, env, tags ✗ always template-owned n/a

launchMode is intentionally NOT stored on session_templates (it's page-level state at template-form time). Project's defaultLaunchMode pre-fills the page-level launchMode when you select the project.

The GET /api/v1/templates list endpoint resolves project values via a single IN-batch query so resolution stays O(1) extra queries no matter the list size. The GET /api/v1/templates/:id endpoint also includes a resolvedProject field with the project metadata.

Project deletion is transactional

DELETE /api/v1/projects/:id runs three operations in a single Drizzle transaction:

  1. UPDATE session_templates SET project_id = NULL WHERE project_id = ?
  2. UPDATE sessions SET project_id = NULL WHERE project_id = ?
  3. DELETE FROM projects WHERE id = ?

If any of the three fails, none apply. Templates and sessions retain their last-saved values; the link is just unset.

Alert rules

Projects can have alert rules that fire when sessions in the project transition state. Five rule types ship in v0.2.0-pre.3:

Rule type Fires when
status_failed A session under this project transitions to status = "failed"
status_completed A session transitions to completed
status_stuck The intelligence classifier marks the session health = "stuck"
no_activity_minutes A non-ended session's lastActivityAt is older than the rule's threshold
freeform_match A small yes/no LLM classifier says an event matches the rule's natural-language condition

Evaluation runs in WatcherRunner's 60-second sweep with a re-entry guard. First-run backfill at rule creation inserts fire rows for every session that already matches the rule's predicate, with no notification dispatched — so a freshly-created status_stuck rule on a project with thirty already-stuck sessions doesn't notification-storm the user. Only state changes that occur AFTER rule creation fire alerts.

Freeform rules carry their own per-rule daily token budget (atomic SQL CASE reset, last-evaluated-event-id cursor, 100-event-per-sweep cap, sample rate) so cost stays bounded. Spend is recorded only on successful classification — an LLM error doesn't drain the budget.

The project_alert_rule_fires table has a UNIQUE constraint on (rule_id, session_id) so each rule fires exactly once per session lifetime. To re-fire on the same session, delete and recreate the rule.

Talking to the projects API

Standard CRUD on /api/v1/projects:

# Create
curl -X POST $BASE/api/v1/projects \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"name":"agentpulse","cwd":"/Users/me/dev/agentpulse","defaultAgentType":"claude_code"}'

# Edit
curl -X PUT $BASE/api/v1/projects/$ID \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"defaultModel":"claude-opus-4-5"}'

# List sessions in the project
curl $BASE/api/v1/projects/$ID/sessions

# Delete (transactional — nulls linked templates/sessions, then removes the project row)
curl -X DELETE $BASE/api/v1/projects/$ID

You can also reach all of this from Ask:

  • "add a project named myapp at /Users/me/dev/myapp" — multi-turn drafting walks you through defaultAgentType, defaultLaunchMode, etc.
  • "edit project agentpulse and change the default model to claude-haiku-4-5" — creates an edit_project action_request for approval.
  • "alert me when any session on agentpulse fails" — creates a create_alert_rule for the project.
  • "delete project myapp" — creates a delete_project action_request showing affected-template and affected-session counts.

See Ask Assistant for the full Ask command surface.

Where the code lives

  • src/server/db/schema.tsprojects, session_templates.project_id, project_alert_rules, project_alert_rule_fires
  • src/server/services/projects/resolver.ts — pure cwd resolver
  • src/server/services/projects/cache.ts — in-process project cache
  • src/server/services/projects/projects-service.ts — CRUD + side-effect re-resolution
  • src/server/services/templates/template-project-resolver.ts — pure template+project merge
  • src/server/services/ai/alert-rule-evaluator.ts — periodic sweep evaluators
  • src/server/routes/projects.ts — REST endpoints
  • src/web/pages/ProjectsPage.tsx/projects UI
  • src/web/components/projects/ — project card + form components

Clone this wiki locally