-
Notifications
You must be signed in to change notification settings - Fork 1
Projects
Projects are first-class records that group sessions and templates by working directory. They were introduced in v0.2.0-pre.3.
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
defaultAgentTypeand every linked template renders the new value on next read.
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.
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.
Saving a template without an explicit projectId triggers ensureProjectForCwd:
- Look up an existing project whose normalized cwd equals the template's cwd. If found, link the template to it.
- Otherwise create a new project with the cwd, derive the name from the cwd basename (with
-2/-3numeric 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.
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.
DELETE /api/v1/projects/:id runs three operations in a single Drizzle transaction:
UPDATE session_templates SET project_id = NULL WHERE project_id = ?UPDATE sessions SET project_id = NULL WHERE project_id = ?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.
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.
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/$IDYou 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_projectaction_request for approval. -
"alert me when any session on agentpulse fails" — creates a
create_alert_rulefor the project. -
"delete project myapp" — creates a
delete_projectaction_request showing affected-template and affected-session counts.
See Ask Assistant for the full Ask command surface.
-
src/server/db/schema.ts—projects,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—/projectsUI -
src/web/components/projects/— project card + form components